{"record":{"id":"6579a0bc5590c6eb","repo":"JuliusBrussee/caveman","slug":"header-q-cannot-be-forwarded","errorCode":null,"errorMessage":"header %q cannot be forwarded","messagePattern":"header %q cannot be forwarded","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/providers/openaicompat/openaicompat.go","lineNumber":404,"sourceCode":"\t\t\tProvider:      \"openai_compatible\",\n\t\t\tBaseURL:       baseURL,\n\t\t\tRoutes:        []string{prefix + \"/\"},\n\t\t\tUsageProvider: wireDialectUsageProvider[wireDialect],\n\t\t},\n\t\tprefix:         prefix,\n\t\tforwardHeaders: append([]string(nil), forwardHeaders...),\n\t\twireDialect:    wireDialect,\n\t}, nil\n}\n\n// ValidateForwardHeaders permits explicit provider-specific headers without\n// letting a mount override routing, message framing, or Caveman credentials.\n// Standard provider authentication is handled by the credential mapper.\nfunc ValidateForwardHeaders(names []string) error {\n\tfor _, name := range names {\n\t\tlower := strings.ToLower(name)\n\t\tif !httpguts.ValidHeaderFieldName(name) || strings.HasPrefix(lower, \"x-cave-\") || strings.HasPrefix(lower, \"x-caveman-\") {\n\t\t\treturn fmt.Errorf(\"header %q cannot be forwarded\", name)\n\t\t}\n\t\tswitch lower {\n\t\t// Routing/framing, credentials, and fields whose value this proxy\n\t\t// constructs. x-forwarded-*/forwarded/x-real-ip would let a caller\n\t\t// choose the client address an upstream rate-limits or allowlists on.\n\t\tcase \"host\", \"connection\", \"keep-alive\", \"proxy-connection\", \"proxy-authorization\", \"proxy-authenticate\", \"te\", \"trailer\", \"transfer-encoding\", \"upgrade\", \"content-length\", \"expect\",\n\t\t\t\"authorization\", \"x-api-key\", \"api-key\", \"x-goog-api-key\", \"x-goog-user-project\", \"cookie\", \"set-cookie\",\n\t\t\t\"forwarded\", \"x-forwarded-for\", \"x-forwarded-host\", \"x-forwarded-proto\", \"x-forwarded-port\", \"x-real-ip\",\n\t\t\t\"user-agent\", \"content-type\":\n\t\t\treturn fmt.Errorf(\"header %q cannot be forwarded\", name)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ValidateName(name string) error {\n\tif !validName.MatchString(name) {\n\t\treturn fmt.Errorf(\"compat upstream name %q must match [a-z0-9][a-z0-9._-]{0,63}\", name)","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/providers/openaicompat/openaicompat.go#L386-L422","documentation":"ValidateForwardHeaders rejects any header name that is not a syntactically valid HTTP field name (httpguts.ValidHeaderFieldName) or that begins with the proxy-reserved prefixes x-cave- or x-caveman-. Such headers would let a mount override routing, message framing, or Caveman credentials, so construction fails with \"header %q cannot be forwarded\".","triggerScenarios":"A name in the forward_headers list fails httpguts.ValidHeaderFieldName (invalid characters, empty) or is prefixed x-cave-/x-caveman- (case-insensitive), evaluated in the first branch of the loop before the denylist switch.","commonSituations":"Header names with spaces or non-ASCII characters from hand-edited config; attempting to spoof the proxy's own x-caveman-* internal headers; copy-pasted names with trailing colons like \"X-Request-Id:\".","solutions":["Remove the x-cave-*/x-caveman-* prefixed header from forward_headers — these are proxy-internal.","Fix the header name syntax: valid token characters only, no colons/spaces, non-empty.","Forward a neutral custom name instead if you need a metadata header.","Validate names with httpguts.ValidHeaderFieldName before adding them to config."],"exampleFix":"// before\nheaders := []string{\"X-Caveman-Trace-Id\"}\n// after\nheaders := []string{\"X-Trace-Id\"}","handlingStrategy":"validation","validationCode":"func forwardable(name string) bool {\n    lower := strings.ToLower(name)\n    return httpguts.ValidHeaderFieldName(name) &&\n        !strings.HasPrefix(lower, \"x-cave-\") &&\n        !strings.HasPrefix(lower, \"x-caveman-\")\n}","typeGuard":"func isProxyReservedHeader(name string) bool {\n    lower := strings.ToLower(name)\n    return strings.HasPrefix(lower, \"x-cave-\") || strings.HasPrefix(lower, \"x-caveman-\")\n}","tryCatchPattern":"if err := openaicompat.ValidateForwardHeaders(headers); err != nil {\n    var bad string\n    fmt.Sscanf(err.Error(), \"header %q\", &bad)\n    headers = slices.DeleteFunc(headers, func(h string) bool { return h == bad })\n}","preventionTips":["Treat x-cave-*/x-caveman-* namespaces as proxy-internal, never forward them.","Validate header tokens before writing them to config.","Strip trailing colons/spaces when importing header lists."],"tags":["go","proxy","headers","security","validation"],"backgroundTag":"invalid-config-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}