fix(failover): do not follow redirects in remote probes

Go resends custom headers such as x-api-key when it follows a redirect,
also to another host, so a redirecting upstream could receive the
target's API key elsewhere. The probe client now treats a redirect as
the response, which fails the probe as a non-2xx status.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-09-27 07:42:20 +00:00
1 parent 132bb216a1
commit 27e5aba60e
2 files changed
+23 -1

No files matched your search

+6 -1
View File
@@ -37,7 +37,12 @@ type DefaultProber struct {
}
func NewProber(loaded LoadedFunc) *DefaultProber {
return &DefaultProber{HTTP: &http.Client{}, Loaded: loaded}
return &DefaultProber{HTTP: &http.Client{
// A redirect is a failed probe, not something to follow: Go resends
// custom headers such as x-api-key to any host, and the target's
// API key must reach only the configured upstream.
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}, Loaded: loaded}
}
func (p *DefaultProber) Liveness(ctx context.Context, cfg config.ModelConfig, kind Kind, warm bool) error {
+17
View File
@@ -125,6 +125,23 @@ var _ = Describe("DefaultProber", func() {
Expect(up.auth).To(Equal("Bearer sekret"))
})
It("does not follow a redirect, so the API key never leaves the upstream", func() {
GinkgoT().Setenv("FAILOVER_PROBE_KEY", "sekret")
other := newFakeUpstream()
DeferCleanup(other.srv.Close)
other.models = []string{"argus-llm"}
redirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, other.srv.URL+r.URL.Path, http.StatusFound)
}))
DeferCleanup(redirect.Close)
c := proxied("argus-llm", "")
c.Proxy.UpstreamURL = redirect.URL + "/v1/chat/completions"
c.Proxy.APIKeyEnv = "FAILOVER_PROBE_KEY"
c.Proxy.Provider = config.ProxyProviderAnthropic
Expect(p.Liveness(ctx, c, KindRemote, false)).To(MatchError(ContainSubstring("302")))
Expect(other.paths).To(BeEmpty())
})
DescribeTable("remote inference hits the usecase endpoint",
func(usecase, path string) {
Expect(p.Inference(ctx, proxied("m", "", usecase), KindRemote, false)).To(Succeed())