{"record":{"id":"b5bb2333c13b0fa3","repo":"Wei-Shaw/sub2api","slug":"token-exchange-failed","errorCode":"token_exchange_failed","errorMessage":"missing access_token","messagePattern":"missing access_token","errorType":"http","errorClass":null,"httpStatus":302,"severity":"error","filePath":"backend/internal/handler/auth_email_oauth.go","lineNumber":495,"sourceCode":"\t\t\t\"grant_type\":    \"authorization_code\",\n\t\t\t\"client_id\":     cfg.ClientID,\n\t\t\t\"client_secret\": cfg.ClientSecret,\n\t\t\t\"code\":          code,\n\t\t\t\"redirect_uri\":  cfg.RedirectURL,\n\t\t}).\n\t\tPost(cfg.TokenURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn nil, fmt.Errorf(\"token endpoint status %d: %s\", resp.StatusCode, truncateLogValue(resp.String(), 1024))\n\t}\n\tvar tokenResp emailOAuthTokenResponse\n\tif err := json.Unmarshal(resp.Bytes(), &tokenResp); err != nil {\n\t\treturn nil, err\n\t}\n\tif strings.TrimSpace(tokenResp.AccessToken) == \"\" {\n\t\treturn nil, errors.New(\"missing access_token\")\n\t}\n\treturn &tokenResp, nil\n}\n\nfunc fetchEmailOAuthProfile(ctx context.Context, provider string, cfg config.EmailOAuthProviderConfig, token *emailOAuthTokenResponse) (*emailOAuthProfile, error) {\n\tresp, err := req.C().\n\t\tR().\n\t\tSetContext(ctx).\n\t\tSetBearerAuthToken(token.AccessToken).\n\t\tSetHeader(\"Accept\", \"application/json\").\n\t\tGet(cfg.UserInfoURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn nil, fmt.Errorf(\"userinfo endpoint status %d: %s\", resp.StatusCode, truncateLogValue(resp.String(), 1024))\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(provider)) {","sourceCodeStart":477,"sourceCodeEnd":513,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/backend/internal/handler/auth_email_oauth.go#L477-L513","documentation":"Thrown after a successful HTTP 2xx response from the OAuth token endpoint when the parsed JSON body has an empty/absent access_token field (code=token_exchange_failed). The endpoint was reached and returned success, but the payload is not a valid token response. Transport errors and non-2xx statuses are separate, earlier failures.","triggerScenarios":"Email OAuth login where the configured TokenURL returns 200 with an error body, an HTML login page, or a JSON structure using a different field name (e.g. missing access_token).","commonSituations":"Wrong TokenURL (pointing at an authorize or userinfo endpoint instead of the token endpoint); a proxy or captive portal intercepting the request; provider API changes; scopes/redirect_uri rejected with a 200 error envelope.","solutions":["Verify the provider's TokenURL in email OAuth config is the real token issuance endpoint (ends in /token, not /authorize)","Log/inspect the raw token endpoint response body (server side it is truncated to 1024 chars in the status-code error path) to see what was actually returned","Confirm client_id/client_secret/redirect_uri and scopes are accepted by the provider","Check for corporate proxies or middleware that rewrite responses"],"exampleFix":"// before (config)\nTokenURL: \"https://provider.com/o/oauth2/auth\"\n// after\nTokenURL: \"https://provider.com/o/oauth2/token\"","handlingStrategy":"validation","validationCode":"// TS client: pre-flight check that the token endpoint is plausible\nfunction assertTokenURL(u: string) {\n  const p = new URL(u);\n  if (!p.pathname.endsWith('/token')) throw new Error(`TokenURL looks wrong: ${u} (expected a /token endpoint)`);\n}","typeGuard":null,"tryCatchPattern":"// Server-side Go: distinguish shape errors from transport errors\nif _, err := exchangeToken(ctx, cfg); err != nil {\n    if err.Error() == \"missing access_token\" {\n        // log raw body (already truncated upstream) and surface a config-hint error\n        log.Warn(\"token endpoint returned no access_token; verify TokenURL\", zap.String(\"url\", cfg.TokenURL))\n    }\n    return err\n}","preventionTips":["Smoke-test the token endpoint with curl before enabling the provider","Assert token URLs end in /token in config validation at startup","Never point TokenURL at an authorize or userinfo endpoint"],"tags":["oauth","authentication","token-exchange","config"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}