JuliusBrussee/caveman · error · ErrProxyInManagedMode
ssrf: Config.Proxy is not supported in managed mode
Error message
ssrf: Config.Proxy is not supported in managed mode
What it means
ErrProxyInManagedMode signals a wiring bug: a Config with ManagedMode=true also set Config.Proxy. Managed mode never routes through a proxy, so instead of silently ignoring the proxy or opening a hole, NewHTTPClient returns an http.Client whose refusingTransport fails every request with this exact error. No network I/O occurs; the security posture is unchanged and only the diagnosis is explicit.
Solutions
- Remove cfg.Proxy when ManagedMode is true — managed mode forbids proxying by design.
- Check with errors.Is(err, ssrf.ErrProxyInManagedMode) and fail fast at startup: detect the contradictory config when building the client rather than at first request.
- If a proxy is genuinely required (e.g. operator egress proxy), run the client in non-managed mode with the proxy hooks, keeping the literal-proxy-address guard.
Example fix
// before
cfg := ssrf.Config{ManagedMode: true, Proxy: http.ProxyFromEnvironment}
client, err := ssrf.NewHTTPClient(cfg) // every request fails with ErrProxyInManagedMode
// after
cfg := ssrf.Config{ManagedMode: true}
if cfg.ManagedMode && cfg.Proxy != nil {
return errors.New("proxy and managed mode are mutually exclusive")
}
client, err := ssrf.NewHTTPClient(cfg) Defensive patterns
Strategy: try-catch
Validate before calling
if cfg.ManagedMode && cfg.Proxy != nil {
return errors.New("ssrf.Config: Proxy and ManagedMode are mutually exclusive")
} Try / catch
client, err := ssrf.NewHTTPClient(cfg)
if err != nil { return err }
resp, err := client.Do(req)
if errors.Is(err, ssrf.ErrProxyInManagedMode) {
return fmt.Errorf("config bug: proxy set while managed mode is on: %w", err)
} Prevention
- Detect the contradictory config at client-construction/startup, not first request
- Gate Proxy selection on the same flag that disables ManagedMode
- Avoid blanket-copying env-derived proxy settings into every client config
When it happens
Trigger: Calling ssrf.NewHTTPClient with a Config where both ManagedMode=true and Proxy != nil, then performing any request — every RoundTrip returns this error wrapped as the request error (checkable with errors.Is).
Common situations: Merging config structs so a proxy set for a non-managed environment bleeds into the managed production client; copying a client-construction snippet that always sets Proxy from env (HTTP_PROXY-derived selector) while forcing managed mode; feature-flag combination (managed + egress proxy) the library disallows.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- ssrf: direct request to the configured proxy address is not…
- compat request URL is missing
- compat route path rejected
- compat route does not match default /compat/ mount
- compat route does not match prefix
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/80c72742116e17a4.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/ssrf/ssrf.go:545
// exactly what it was; only the diagnosis changes.
return &http.Client{Transport: refusingTransport{err: ErrProxyInManagedMode}}
}
t.Proxy, t.DialContext = proxiedHooks(cfg, t.DialContext)
}
return &http.Client{
Transport: t,
// Provider and webhook clients must not carry credentials across redirects.
// Callers that intentionally implement redirects (for example OIDC) must
// validate every hop explicitly and use their own bounded policy.
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
// ErrProxyInManagedMode reports a wiring bug: a managed-mode Config carrying a
// Proxy selector. Managed mode never proxies (see Config.Proxy).
var ErrProxyInManagedMode = errors.New("ssrf: Config.Proxy is not supported in managed mode")
// refusingTransport fails every request with one explanatory error instead of
// letting a client built from a contradictory Config look like it works.
type refusingTransport struct{ err error }
func (t refusingTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }
// proxiedHooks returns the Transport.Proxy and DialContext pair for a client
// with cfg.Proxy set. The proxy hook validates what it can about the request
// destination without DNS (the proxy resolves hostnames, often on a network the
// client cannot see), then remembers the proxy address it chose so the dial hook
// can pass that one address through unguarded. Every other address — including
// a direct dial when cfg.Proxy returns nil, e.g. a NO_PROXY match — still goes
// through the full guard. The dial hook cannot tell a proxied dial from a direct
// one to the same host:port, so a direct request whose destination collides with
// a remembered proxy address is refused here instead of reaching that pass-through.
func proxiedHooks(cfg Config, guarded func(context.Context, string, string) (net.Conn, error)) (func(*http.Request) (*url.URL, error), func(context.Context, string, string) (net.Conn, error)) {
var proxyAddrs sync.Map // host:port as Transport dials it → struct{}View on GitHub (pinned to 3ee70a1026)