{"record":{"id":"746ec5fb97d7d48e","repo":"JuliusBrussee/caveman","slug":"gemini-bearer-credential-requires-a-valid-x-goog-u","errorCode":null,"errorMessage":"gemini bearer credential requires a valid x-goog-user-project","messagePattern":"gemini bearer credential requires a valid x-goog-user-project","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/providers/adapter.go","lineNumber":476,"sourceCode":"\t\tif out.Get(\"anthropic-version\") == \"\" {\n\t\t\tout.Set(\"anthropic-version\", \"2023-06-01\")\n\t\t}\n\tcase \"gemini\":\n\t\tif credential.Key != \"\" {\n\t\t\tif credential.Scheme == \"bearer\" {\n\t\t\t\t// Standalone env fallback uses this synthetic marker to reach the\n\t\t\t\t// post-sanitization key resolver. It is not OAuth and must not be\n\t\t\t\t// forwarded or subjected to OAuth quota-project validation.\n\t\t\t\tif credential.Key == \"no-key-required\" {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\t// Gemini CLI OAuth/ADC credentials are bearer tokens. Remapping one to\n\t\t\t\t// x-goog-api-key breaks authentication and silently defeats OAuth wrap.\n\t\t\t\t// Google also requires the caller's quota project for user OAuth. Never\n\t\t\t\t// guess it: wrong attribution is a billing and quota correctness bug.\n\t\t\t\tquotaProject := strings.TrimSpace(req.Header.Get(\"x-goog-user-project\"))\n\t\t\t\tif !validGoogleQuotaProject(quotaProject) {\n\t\t\t\t\treturn nil, fmt.Errorf(\"gemini bearer credential requires a valid x-goog-user-project\")\n\t\t\t\t}\n\t\t\t\tout.Set(\"authorization\", \"Bearer \"+credential.Key)\n\t\t\t\tout.Set(\"x-goog-user-project\", quotaProject)\n\t\t\t} else {\n\t\t\t\tout.Set(\"x-goog-api-key\", credential.Key)\n\t\t\t}\n\t\t}\n\tcase \"vertex\":\n\t\tif credential.Key != \"\" {\n\t\t\tout.Set(\"authorization\", \"Bearer \"+credential.Key)\n\t\t}\n\t\tcopyIfPresent(out, req.Header, \"x-vertex-ai-llm-request-type\")\n\t\tcopyIfPresent(out, req.Header, \"x-goog-user-project\")\n\tcase \"azure_openai\":\n\t\tif credential.Key != \"\" {\n\t\t\t// The standalone placeholder is a synthetic no-key marker used by\n\t\t\t// callers that explicitly allow env-backed API-key fallback. It is\n\t\t\t// not an Entra token and must not be forwarded as Authorization;","sourceCodeStart":458,"sourceCodeEnd":494,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/proxy/providers/adapter.go#L458-L494","documentation":"For Gemini with a bearer-scheme credential (Gemini CLI OAuth/ADC), the adapter must send x-goog-user-project so Google attributes the call to the caller's quota/billing project. The code refuses to guess: if the inbound x-goog-user-project header is absent or fails validGoogleQuotaProject, the request is rejected. Remapping a bearer token to x-goog-api-key instead would break auth and mis-attribute quota, so this is a correctness fail-closed guard.","triggerScenarios":"Routing Gemini CLI (OAuth) traffic through the proxy without the client sending x-goog-user-project; the header present but empty, whitespace-padded, or malformed so validation fails; using an ADC/bearer credential where an API-key credential was intended.","commonSituations":"Switching a client from an API key to OAuth login and forgetting quota-project config; the header name typo'd by an intermediate proxy; GOOGLE_CLOUD_PROJECT set in env but the header never attached to proxied requests.","solutions":["Have the client send a valid quota project header: x-goog-user-project: my-gcp-project on Gemini OAuth requests through the proxy.","If you never intended OAuth, configure an API-key credential instead — the API-key branch sets x-goog-api-key and never hits this check.","Ensure intermediaries forward the x-goog-user-project header (check droplists) and that its value is a real project id."],"exampleFix":"# before\ncurl http://proxy:8080/gemini/v1/... -H \"authorization: Bearer $OAUTH_TOKEN\"\n# -> error: bearer credential requires x-goog-user-project\n\n# after\ncurl http://proxy:8080/gemini/v1/... \\\n  -H \"authorization: Bearer $OAUTH_TOKEN\" \\\n  -H \"x-goog-user-project: my-gcp-project\"","handlingStrategy":"validation","validationCode":"// Before sending a Gemini OAuth request through the proxy:\nqp := strings.TrimSpace(req.Header.Get(\"x-goog-user-project\"))\nif qp == \"\" || strings.ContainsAny(qp, \" /\") {\n    return errors.New(\"Gemini OAuth requests need a valid x-goog-user-project header\")\n}\n// prefer a real project id: letters, digits, hyphens\nif !projectIDRe.MatchString(qp) {\n    return fmt.Errorf(\"x-goog-user-project %q is not a valid project id\", qp)\n}","typeGuard":"var projectIDRe = regexp.MustCompile(`^[a-z][a-z0-9-]{4,28}[a-z0-9]$`)\n\nfunc hasValidQuotaProject(h http.Header) bool {\n    v := strings.TrimSpace(h.Get(\"x-goog-user-project\"))\n    return projectIDRe.MatchString(v)\n}","tryCatchPattern":"if _, err := adapter.BuildUpstreamHeaders(credential, req); err != nil {\n    if strings.Contains(err.Error(), \"x-goog-user-project\") {\n        http.Error(w, \"Gemini OAuth requires the x-goog-user-project header (your GCP quota project)\", http.StatusBadRequest)\n        return\n    }\n    http.Error(w, err.Error(), http.StatusBadRequest)\n}","preventionTips":["Configure the quota project wherever OAuth credentials are configured.","Use API-key credentials when OAuth attribution is not needed.","Ensure intermediaries forward x-goog-user-project."],"tags":["go","auth","oauth","gemini","billing","routing"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}