{"record":{"id":"841c8e40fbe78c77","repo":"JuliusBrussee/caveman","slug":"provider-q-upstream-url-must-be-absolute","errorCode":null,"errorMessage":"provider %q upstream URL must be absolute","messagePattern":"provider %q upstream URL must be absolute","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/providers/adapter.go","lineNumber":415,"sourceCode":"\t\t}\n\t}\n\treturn false\n}\n\nfunc (b Base) ResolveUpstreamURL(ctx context.Context, req *http.Request, route RouteContext) (*url.URL, error) {\n\tbaseURL := b.BaseURL\n\tif route.BaseURL != \"\" {\n\t\tbaseURL = route.BaseURL\n\t}\n\tif strings.TrimSpace(baseURL) == \"\" {\n\t\treturn nil, fmt.Errorf(\"provider %q has no configured upstream URL\", b.Provider)\n\t}\n\tbase, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !base.IsAbs() || base.Hostname() == \"\" {\n\t\treturn nil, fmt.Errorf(\"provider %q upstream URL must be absolute\", b.Provider)\n\t}\n\tpath := req.URL.Path\n\tfor _, prefix := range []string{\"/openai\", \"/anthropic\", \"/gemini\", \"/compat/stub\", \"/compat/openai-compatible\"} {\n\t\tpath = strings.TrimPrefix(path, prefix)\n\t}\n\tif strings.HasPrefix(req.URL.Path, \"/azure/\") {\n\t\tpath = strings.TrimPrefix(req.URL.Path, \"/azure\")\n\t}\n\tbase.Path = strings.TrimRight(base.Path, \"/\") + path\n\tbase.RawQuery = req.URL.RawQuery\n\treturn base, nil\n}\n\nfunc (b Base) SanitizeAndMapHeaders(ctx context.Context, req *http.Request, credential Credential, _ *url.URL) (http.Header, error) {\n\tout := http.Header{}\n\tcopyIfPresent(out, req.Header, \"content-type\")\n\tcopyIfPresent(out, req.Header, \"accept\")\n\tcopyIfPresent(out, req.Header, \"accept-encoding\")","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/proxy/providers/adapter.go#L397-L433","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Write the full absolute URL including scheme and host: base_url: https://api.openai.com.","Validate config at load: u, err := url.Parse(v); err == nil && u.IsAbs() && u.Hostname() != \"\" — reject early with the provider name.","Check for shell/YAML mangling (quotes, comments) that truncates the value to a relative fragment."],"exampleFix":"# before\nbase_url: api.openai.com\n\n# after\nbase_url: https://api.openai.com","handlingStrategy":"validation","validationCode":"func isAbsoluteHTTPURL(raw string) bool {\n    u, err := url.Parse(strings.TrimSpace(raw))\n    return err == nil && (u.Scheme == \"http\" || u.Scheme == \"https\") && u.Hostname() != \"\"\n}\n\nif !isAbsoluteHTTPURL(baseURL) {\n    return fmt.Errorf(\"base_url %q must be an absolute http(s) URL\", baseURL)\n}","typeGuard":"func isAbsoluteHTTPURL(raw string) bool {\n    u, err := url.Parse(strings.TrimSpace(raw))\n    return err == nil && (u.Scheme == \"http\" || u.Scheme == \"https\") && u.Hostname() != \"\"\n}","tryCatchPattern":"if _, err := adapter.ResolveUpstreamURL(ctx, req, route); err != nil {\n    if strings.Contains(err.Error(), \"must be absolute\") {\n        http.Error(w, \"provider base URL must include scheme and host\", http.StatusBadGateway)\n        return\n    }\n    http.Error(w, err.Error(), http.StatusBadRequest)\n}","preventionTips":["Always write base URLs with scheme://host.","Lint config: url.Parse + IsAbs + Hostname checks at load time.","Beware shell/env truncation that strips the scheme."],"tags":["go","configuration","url-parsing","routing"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}