{"record":{"id":"bec44c8fa19c1103","repo":"sipeed/picoclaw","slug":"provider-q-does-not-support-browser-oauth","errorCode":null,"errorMessage":"provider %q does not support browser oauth","messagePattern":"provider %q does not support browser oauth","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/oauth.go","lineNumber":273,"sourceCode":"\t\th.storeOAuthFlow(flow)\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(map[string]any{\n\t\t\t\"status\":     \"ok\",\n\t\t\t\"provider\":   provider,\n\t\t\t\"method\":     method,\n\t\t\t\"flow_id\":    flow.ID,\n\t\t\t\"user_code\":  flow.UserCode,\n\t\t\t\"verify_url\": flow.VerifyURL,\n\t\t\t\"interval\":   flow.Interval,\n\t\t\t\"expires_at\": flow.ExpiresAt.Format(time.RFC3339),\n\t\t})\n\t\treturn\n\n\tcase oauthMethodBrowser:\n\t\tcfg, err := oauthConfigForProvider(provider)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tpkce, err := oauthGeneratePKCE()\n\t\tif err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"failed to generate PKCE: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tstate, err := oauthGenerateState()\n\t\tif err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"failed to generate state: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tredirectURI := buildOAuthRedirectURI(r)\n\t\tauthURL := oauthBuildAuthorizeURL(cfg, pkce, state, redirectURI)\n\n\t\tnow := oauthNow()","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L255-L291","documentation":"Returned as HTTP 400 by POST /api/oauth/login (browser method) via oauthConfigForProvider: only \"openai\" and \"google-antigravity\" have browser OAuth configs (auth.OpenAIOAuthConfig / auth.GoogleAntigravityOAuthConfig); the default branch of the switch produces this message. With the current method matrix it is effectively a defensive guard, because isOAuthMethodSupported already rejects browser login for anthropic earlier with error 961 — you can only reach 965 if the matrix and the config switch have drifted (e.g. browser was added to anthropic's methods without adding a config).","triggerScenarios":"POST /api/oauth/login {\"provider\":\"anthropic\",\"method\":\"browser\"} in a build where oauthProviderMethods lists browser for anthropic but oauthConfigForProvider still has no anthropic case; or any new provider added to the methods map with browser support but no branch in oauthConfigForProvider.","commonSituations":"Local fork or PR that adds a provider's browser method but forgets the config switch; regression after refactoring oauthProviderMethods; hitting an older backend with a newer frontend that assumes a method exists.","solutions":["If you are a caller: this should normally be unreachable — first check that the method matrix really allows browser for the provider, then upgrade/redeploy the backend so matrix and config agree.","If you are modifying the backend: every provider you add to oauthProviderMethods[provider] with \"browser\" must also get a case in oauthConfigForProvider returning an auth.OAuthProviderConfig.","Add a unit test asserting each provider in oauthProviderMethods with browser support passes oauthConfigForProvider without error, so the drift is caught at build time."],"exampleFix":"// before (web/backend/api/oauth.go)\nvar oauthProviderMethods = map[string][]string{\n    oauthProviderAnthropic: {oauthMethodBrowser, oauthMethodToken}, // browser advertised...\n}\nfunc oauthConfigForProvider(provider string) (auth.OAuthProviderConfig, error) {\n    switch provider {\n    case oauthProviderOpenAI: return auth.OpenAIOAuthConfig(), nil\n    case oauthProviderGoogleAntigravity: return auth.GoogleAntigravityOAuthConfig(), nil\n    default: return auth.OAuthProviderConfig{}, fmt.Errorf(\"provider %q does not support browser oauth\", provider) // ...but no config\n    }\n}\n\n// after: add the matching case\nfunc oauthConfigForProvider(provider string) (auth.OAuthProviderConfig, error) {\n    switch provider {\n    case oauthProviderOpenAI: return auth.OpenAIOAuthConfig(), nil\n    case oauthProviderGoogleAntigravity: return auth.GoogleAntigravityOAuthConfig(), nil\n    case oauthProviderAnthropic: return auth.AnthropicOAuthConfig(), nil\n    default: return auth.OAuthProviderConfig{}, fmt.Errorf(\"provider %q does not support browser oauth\", provider)\n    }\n}","handlingStrategy":"validation","validationCode":"const BROWSER_CAPABLE = new Set(['openai', 'google-antigravity']);\nfunction assertBrowserSupported(provider) {\n  if (!BROWSER_CAPABLE.has(provider)) throw new Error(`provider ${JSON.stringify(provider)} does not support browser oauth`);\n}","typeGuard":"function supportsBrowserOauth(provider) {\n  return ['openai', 'google-antigravity'].includes(provider);\n}","tryCatchPattern":null,"preventionTips":["Only offer 'browser' for providers whose methods list (from /api/oauth/providers) includes it.","If you fork the backend, keep oauthProviderMethods and oauthConfigForProvider in sync — add a test that every browser-capable provider returns a config."],"tags":["oauth","http-400","browser-oauth","invariant","config-drift"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}