{"record":{"id":"ecfe871d70cde7b2","repo":"plandex-ai/plandex","slug":"token-exchange-failed-status-d-body-s","errorCode":null,"errorMessage":"token exchange failed - status: %d, body: %s","messagePattern":"token exchange failed - status: (.+?), body: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/cli/lib/claude_max.go","lineNumber":258,"sourceCode":"\treq, err := http.NewRequest(\"POST\", claudeMaxTokenUrl, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token exchange failed - error creating request: %s\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"anthropic-beta\", shared.AnthropicClaudeMaxBetaHeader)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"token exchange failed - error reading body: %s\", err)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"token exchange failed - status: %d, body: %s\", resp.StatusCode, b)\n\t}\n\tvar t types.OauthResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&t); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &t, nil\n}\n\nfunc genCodeVerifier() (string, error) {\n\tbuf := make([]byte, 32)\n\tif _, err := rand.Read(buf); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(buf), nil\n}\n\nfunc sha256Base64(verifier string) string {\n\tsum := sha256.Sum256([]byte(verifier))","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/cli/lib/claude_max.go#L240-L276","documentation":"This is the standard non-200 response error from the Claude Max OAuth token exchange. When the token endpoint returns any status other than 200, exchangeCode reads the response body and returns it verbatim inside this error so the caller can see the server's rejection reason.","triggerScenarios":"POST to claudeMaxTokenUrl completes but returns a non-200 status — invalid/expired authorization code (400/401), bad client_id or redirect_uri mismatch, rate limiting (429), or server-side errors (5xx).","commonSituations":"The authorization code was already redeemed or expired (codes are single-use and short-lived); redirect_uri does not exactly match the one used in the authorize step; wrong or outdated client_id; Anthropic API outage or rate limiting.","solutions":["Read the returned status and body — 400/401 with 'invalid_grant' means restart the whole OAuth flow to get a fresh code.","Verify redirect_uri and client_id exactly match the values used in the authorization request.","Do not reuse authorization codes; exchange the code immediately after receiving it.","On 429/5xx, retry after a delay."],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"token exchange failed - status: %d, body: %s\", resp.StatusCode, b)\n// after\nif resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {\n\treturn nil, retryable{fmt.Errorf(\"token exchange failed - status: %d, body: %s\", resp.StatusCode, b)}\n}\nreturn nil, fmt.Errorf(\"token exchange failed - status: %d, body: %s\", resp.StatusCode, b)","handlingStrategy":"retry","validationCode":"// exchange codes immediately and only once\nif time.Since(codeReceivedAt) > 5*time.Minute {\n\t// code expired: re-run authorization instead of exchanging\n}\n// ensure redirect_uri matches the authorize request exactly\nif redirectURI != authorizeRedirectURI { /* abort: guaranteed 400 */ }","typeGuard":"func isTokenRejection(err error) bool { return strings.Contains(err.Error(), \"status: 4\") }","tryCatchPattern":"tok, err := exchangeCode(ctx, code, verifier)\nif err != nil {\n\tvar status int\n\tif _, serr := fmt.Sscanf(err.Error(), \"token exchange failed - status: %d\", &status); serr == nil && (status == 429 || status >= 500) {\n\t\t// retry with backoff\n\t} else if status >= 400 && status < 500 {\n\t\t// re-run full OAuth flow for a fresh code\n\t}\n}","preventionTips":["Exchange authorization codes immediately — they are single-use and short-lived.","Keep redirect_uri and client_id byte-identical between authorize and token requests.","Never reuse a code after a failed exchange attempt.","Implement backoff retry for 429/5xx responses."],"tags":["go","oauth","http","token-exchange","api"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}