JuliusBrussee/caveman · error

provider %q upstream URL must be absolute

Error message

provider %q upstream URL must be absolute

What it means

After establishing a non-empty base URL, Base.ResolveUpstreamURL parses it and requires the result to be absolute with a hostname. A value like 'api.openai.com' (no scheme), '/v1' (relative), or 'file:///path' (no host) fails this check. The proxy joins request paths onto this URL, so a relative base would produce an un-dialable target.

Source

Thrown at proxy/providers/adapter.go:415

		}
	}
	return false
}

func (b Base) ResolveUpstreamURL(ctx context.Context, req *http.Request, route RouteContext) (*url.URL, error) {
	baseURL := b.BaseURL
	if route.BaseURL != "" {
		baseURL = route.BaseURL
	}
	if strings.TrimSpace(baseURL) == "" {
		return nil, fmt.Errorf("provider %q has no configured upstream URL", b.Provider)
	}
	base, err := url.Parse(baseURL)
	if err != nil {
		return nil, err
	}
	if !base.IsAbs() || base.Hostname() == "" {
		return nil, fmt.Errorf("provider %q upstream URL must be absolute", b.Provider)
	}
	path := req.URL.Path
	for _, prefix := range []string{"/openai", "/anthropic", "/gemini", "/compat/stub", "/compat/openai-compatible"} {
		path = strings.TrimPrefix(path, prefix)
	}
	if strings.HasPrefix(req.URL.Path, "/azure/") {
		path = strings.TrimPrefix(req.URL.Path, "/azure")
	}
	base.Path = strings.TrimRight(base.Path, "/") + path
	base.RawQuery = req.URL.RawQuery
	return base, nil
}

func (b Base) SanitizeAndMapHeaders(ctx context.Context, req *http.Request, credential Credential, _ *url.URL) (http.Header, error) {
	out := http.Header{}
	copyIfPresent(out, req.Header, "content-type")
	copyIfPresent(out, req.Header, "accept")
	copyIfPresent(out, req.Header, "accept-encoding")

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Write the full absolute URL including scheme and host: base_url: https://api.openai.com.
  2. Validate config at load: u, err := url.Parse(v); err == nil && u.IsAbs() && u.Hostname() != "" — reject early with the provider name.
  3. Check for shell/YAML mangling (quotes, comments) that truncates the value to a relative fragment.

Example fix

# before
base_url: api.openai.com

# after
base_url: https://api.openai.com
Defensive patterns

Strategy: validation

Validate before calling

func isAbsoluteHTTPURL(raw string) bool {
    u, err := url.Parse(strings.TrimSpace(raw))
    return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != ""
}

if !isAbsoluteHTTPURL(baseURL) {
    return fmt.Errorf("base_url %q must be an absolute http(s) URL", baseURL)
}

Type guard

func isAbsoluteHTTPURL(raw string) bool {
    u, err := url.Parse(strings.TrimSpace(raw))
    return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != ""
}

Try / catch

if _, err := adapter.ResolveUpstreamURL(ctx, req, route); err != nil {
    if strings.Contains(err.Error(), "must be absolute") {
        http.Error(w, "provider base URL must include scheme and host", http.StatusBadGateway)
        return
    }
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Prevention

When it happens

Trigger: Configuring base_url: api.openai.com/v1 without https://; using a leading-slash relative path; a base URL of just 'http://' with the host in a different field; copy-paste dropping the scheme.

Common situations: Operators trimming 'the obvious prefix' from docs examples; docker env values losing the scheme to shell parsing; mixing up host-only fields and full-URL fields when porting config from another tool.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/841c8e40fbe78c77. Report an issue: GitHub.