plandex-ai/plandex · error
token exchange failed - status: %d, body: %s
Error message
token exchange failed - status: %d, body: %s
What it means
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.
Source
Thrown at app/cli/lib/claude_max.go:258
req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("token exchange failed - error creating request: %s", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-beta", shared.AnthropicClaudeMaxBetaHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("token exchange failed - error reading body: %s", err)
}
return nil, fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b)
}
var t types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
return nil, err
}
return &t, nil
}
func genCodeVerifier() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func sha256Base64(verifier string) string {
sum := sha256.Sum256([]byte(verifier))View on GitHub (pinned to e2d772072e)
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.
Example fix
// before
return nil, fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b)
// after
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
return nil, retryable{fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b)}
}
return nil, fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b) Defensive patterns
Strategy: retry
Validate before calling
// exchange codes immediately and only once
if time.Since(codeReceivedAt) > 5*time.Minute {
// code expired: re-run authorization instead of exchanging
}
// ensure redirect_uri matches the authorize request exactly
if redirectURI != authorizeRedirectURI { /* abort: guaranteed 400 */ } Type guard
func isTokenRejection(err error) bool { return strings.Contains(err.Error(), "status: 4") } Try / catch
tok, err := exchangeCode(ctx, code, verifier)
if err != nil {
var status int
if _, serr := fmt.Sscanf(err.Error(), "token exchange failed - status: %d", &status); serr == nil && (status == 429 || status >= 500) {
// retry with backoff
} else if status >= 400 && status < 500 {
// re-run full OAuth flow for a fresh code
}
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- token exchange failed - error creating request: %s
- token exchange failed - error reading body: %s
- failed to update context: %v
- Custom model providers are not supported on Plandex Cloud
- error listing contexts: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/ecfe871d70cde7b2.
Report an issue: GitHub.