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

  1. Use one of: upstream_proxy: env, upstream_proxy: off, or a full URL like http://proxy.corp.local:3128
  2. Add the scheme to bare host:port values (http://, https://, socks5://, or socks5h://)
  3. Quote the URL in YAML if it contains special characters (e.g. socks5h://user:pass@host:1080)
  4. 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

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


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)