plandex-ai/plandex · error
token exchange failed - error reading body: %s
Error message
token exchange failed - error reading body: %s
What it means
During the Claude Max OAuth code exchange, if the token endpoint responds with a non-200 status, the code attempts to read the response body so it can build a more informative status error. This error is returned only when that io.ReadAll(resp.Body) itself fails.
Source
Thrown at app/cli/lib/claude_max.go:256
"client_id": claudeMaxClientId,
})
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
}
View on GitHub (pinned to e2d772072e)
Solutions
- Retry the token exchange — the failure was in reading the error body, so the root status is unknown and transient network issues are the likely cause.
- Check connectivity/proxy settings between the client and claudeMaxTokenUrl.
- Log resp.StatusCode even when the body read fails, so the status isn't lost.
- Check the Anthropic status page for an ongoing outage.
Example fix
// before
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)
}
// after
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("token exchange failed - status: %d, error reading body: %w", resp.StatusCode, err)
} Defensive patterns
Strategy: retry
Validate before calling
client := &http.Client{Timeout: 15 * time.Second}
// pre-flight reachability
if resp, err := client.Head(claudeMaxTokenUrl); err != nil { /* warn: endpoint unreachable */ } else { resp.Body.Close() } Type guard
func isBodyReadErr(err error) bool { return strings.Contains(err.Error(), "error reading body") } Try / catch
for i := 0; i < 3; i++ {
tok, err := exchangeCode(ctx, code, verifier)
if err != nil && strings.Contains(err.Error(), "error reading body") {
time.Sleep(time.Duration(1<<i) * time.Second)
continue
}
break
} Prevention
- Set an explicit http.Client timeout instead of DefaultClient.
- Retry transient body-read failures with backoff.
- Check proxy/VPN stability when exchanging tokens.
- Log resp.StatusCode even when body reading fails.
When it happens
Trigger: The token endpoint returned non-200 AND reading the error response body failed — network interruption mid-response, truncated/chunked response, or the server closed the connection before the body was fully received.
Common situations: Flaky network or VPN dropping the connection to the token endpoint; proxy terminating the response early; Claude API outage returning an incomplete error response.
Related errors
- token exchange failed - error creating request: %s
- token exchange failed - status: %d, body: %s
- refresh failed - http: %w
- failed to update context: %v
- failed to download the update: %w
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/2dac806aa71dbd0c.
Report an issue: GitHub.