JuliusBrussee/caveman · error
upstream_proxy must be "env", "off" or a proxy URL
Error message
upstream_proxy %q must be "env", "off" or a proxy URL
What it means
parseUpstreamProxy (proxy/internal/config/config.go:270) accepts exactly three kinds of upstream_proxy values: the literal "env" (use HTTPS_PROXY/NO_PROXY), "off" (direct, never proxied), or a full proxy URL with a host. Anything else — unparseable URL, missing host, empty string — is rejected with this error before any network traffic happens, so an operator cannot silently lose proxying.
Solutions
- Use one of: upstream_proxy: env, upstream_proxy: off, or a full URL like http://proxy.corp.local:3128
- Add the scheme to bare host:port values (http://, https://, socks5://, or socks5h://)
- Quote the URL in YAML if it contains special characters (e.g. socks5h://user:pass@host:1080)
- Unset empty CAVE_UPSTREAM_PROXY values; an empty string is not a valid setting
Example fix
// before (caveman.yaml) upstream_proxy: proxy.corp.local:3128 // after upstream_proxy: http://proxy.corp.local:3128
Defensive patterns
Strategy: validation
Validate before calling
func validUpstreamProxy(raw string) bool {
switch raw {
case "", "env", "off":
return raw != "" || true
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" { return false }
switch u.Scheme {
case "http", "https", "socks5", "socks5h":
return true
}
return false
} Prevention
- Only use the literal keywords env or off, or a fully-schemed URL
- Always include a scheme (http://, https://, socks5://, socks5h://) on proxy addresses
- Quote URLs with credentials/special chars in YAML
- Unset (rather than empty-set) CAVE_UPSTREAM_PROXY when not using a proxy
When it happens
Trigger: upstream_proxy in caveman.yaml or CAVE_UPSTREAM_PROXY set to a typo like 'htp://proxy:8080', a bare 'proxy:8080' with no scheme, an empty value, or whitespace-only text; any value besides env/off that url.Parse rejects or that lacks a Host.
Common situations: Writing 'proxy.corp.local:3128' without a scheme; YAML unquoting issues mangling the URL; setting the env var to empty in a shell profile; expecting 'system' or 'auto' to be valid keywords when only 'env' and 'off' exist.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- AbortError
- cave response was not valid JSON
- caveman trial proxy did not become ready on
- compat upstream name
- compat upstream name
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e4269747e89db29d.
Report an issue: GitHub.
Appendix: source
Thrown at proxy/internal/config/config.go:270
return nil
}
return fn
}
func parseUpstreamProxy(raw string) (func(*http.Request) (*url.URL, error), error) {
raw = strings.TrimSpace(raw)
switch strings.ToLower(raw) {
case "", "env":
// ProxyFromEnvironment snapshots the proxy variables once per process
// (sync.Once), so a test that t.Setenv's HTTPS_PROXY must build its own
// httpproxy.Config selector instead — see ssrf_test.go.
return http.ProxyFromEnvironment, nil
case "off":
return nil, nil
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return nil, fmt.Errorf("upstream_proxy %q must be \"env\", \"off\" or a proxy URL", raw)
}
switch u.Scheme {
case "http", "https", "socks5", "socks5h":
default:
return nil, fmt.Errorf("upstream_proxy %q: unsupported scheme %q", raw, u.Scheme)
}
// Same selector semantics as env mode: localhost/loopback destinations (an
// allowlisted Ollama) and NO_PROXY matches are dialed direct rather than
// handed to a corporate proxy that cannot reach them.
selector := (&httpproxy.Config{HTTPProxy: raw, HTTPSProxy: raw, NoProxy: env.String("NO_PROXY", env.String("no_proxy", ""))}).ProxyFunc()
return func(req *http.Request) (*url.URL, error) { return selector(req.URL) }, nil
}
// minAuthTokenBytes is the floor for the inbound shared secret. The token is the
// only gate in front of every configured provider credential once the proxy is
// reachable off-host, so a short one is not a weaker deployment, it is an open one.
const minAuthTokenBytes = 16
View on GitHub (pinned to 3ee70a1026)