p3: tavily parse errors degrade; retry DNS on search
A 200 with a bad Tavily body is tavily: error, not an empty success that would cache for the hour. Upstream clients stay in the source list if DNS is down at boot and retry the pin on Search; /healthz lists them as unresolved until a pin lands.
This commit is contained in:
@@ -77,9 +77,7 @@ func New(raw string, opt Options) (*Client, error) {
|
||||
lookup: lookup,
|
||||
refresh: opt.Refresh,
|
||||
}
|
||||
if err := c.resolve(context.Background()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = c.resolve(context.Background()) // DNS down: keep the client, retry in Search
|
||||
d := &net.Dialer{
|
||||
Timeout: opt.Timeout,
|
||||
Control: c.control,
|
||||
@@ -204,17 +202,38 @@ func (c *Client) checkRedirect(req *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshIfStale re-resolves when the pin is older than Refresh.
|
||||
// RefreshIfStale re-resolves when unpinned or the pin is older than Refresh.
|
||||
// A failed refresh leaves existing pins in place.
|
||||
func (c *Client) RefreshIfStale(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
stale := time.Since(c.resolved) >= c.refresh
|
||||
need := len(c.ips) == 0 || time.Since(c.resolved) >= c.refresh
|
||||
c.mu.Unlock()
|
||||
if !stale {
|
||||
if !need {
|
||||
return nil
|
||||
}
|
||||
return c.resolve(ctx)
|
||||
}
|
||||
|
||||
// Ensure resolves if needed. Error only when still unpinned afterwards.
|
||||
func (c *Client) Ensure(ctx context.Context) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("search dial: nil")
|
||||
}
|
||||
if err := c.RefreshIfStale(ctx); err != nil && !c.Ready() {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Ready() bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.ips) > 0
|
||||
}
|
||||
|
||||
func (c *Client) IPs() []netip.Addr {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -2,16 +2,20 @@ package dial_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"git.wntrmute.dev/kyle/sift/internal/search/dial"
|
||||
)
|
||||
|
||||
var errDNS = errors.New("dns down")
|
||||
|
||||
func TestLoopbackAllowsConfiguredPort(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
@@ -53,25 +57,31 @@ func TestLoopbackRefusesOtherPort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublicPoisonedPrivateRefused(t *testing.T) {
|
||||
_, err := dial.New("https://api.tavily.com/search", dial.Options{
|
||||
c, err := dial.New("https://api.tavily.com/search", dial.Options{
|
||||
Loopback: false,
|
||||
Lookup: func(_ context.Context, host string) ([]netip.Addr, error) {
|
||||
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("poisoned A record allowed")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Ready() {
|
||||
t.Fatal("poisoned A record pinned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicPrivateRFC1918Refused(t *testing.T) {
|
||||
_, err := dial.New("https://api.search.brave.com/res/v1/llm/context", dial.Options{
|
||||
c, err := dial.New("https://api.search.brave.com/res/v1/llm/context", dial.Options{
|
||||
Lookup: func(_ context.Context, _ string) ([]netip.Addr, error) {
|
||||
return []netip.Addr{netip.MustParseAddr("10.0.0.5")}, nil
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("private A allowed")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Ready() {
|
||||
t.Fatal("private A pinned")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,3 +120,39 @@ func TestParseRejectsNonHTTP(t *testing.T) {
|
||||
}
|
||||
_ = url.URL{}
|
||||
}
|
||||
|
||||
func TestResolveRetriesAfterDNSDown(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer ts.Close()
|
||||
u, _ := url.Parse(ts.URL)
|
||||
ip := netip.MustParseAddr(u.Hostname())
|
||||
var n atomic.Int32
|
||||
c, err := dial.New(ts.URL, dial.Options{
|
||||
Loopback: true,
|
||||
Lookup: func(_ context.Context, host string) ([]netip.Addr, error) {
|
||||
if n.Add(1) == 1 {
|
||||
return nil, errDNS
|
||||
}
|
||||
return []netip.Addr{ip}, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Ready() {
|
||||
t.Fatal("first lookup should fail")
|
||||
}
|
||||
if err := c.Ensure(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !c.Ready() {
|
||||
t.Fatal("retry")
|
||||
}
|
||||
resp, err := c.HTTP.Get(ts.URL + "/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user