router-for-me/CLIProxyAPI · error
token exchange failed with status %d: %s
Error message
token exchange failed with status %d: %s
What it means
The Codex token endpoint answered the exchange request with a non-200 status; the message includes the status code and the raw response body. This is the provider rejecting the exchange itself — the authorization code was invalid/expired/already used, the PKCE verifier did not match the challenge, the redirect_uri differed from the one in the authorization request, or the endpoint rate-limited the client.
Source
Thrown at internal/auth/codex/openai_auth.go:139
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := o.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("token exchange request failed: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token response: %w", err)
}
// log.Debugf("Token response: %s", string(body))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse token response
var tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
if err = json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
// Extract account ID from ID token
claims, err := ParseJWTToken(tokenResp.IDToken)
if err != nil {View on GitHub (pinned to 78f0c4079e)
Solutions
- Read the status and body in the message: 400 + invalid_grant means the code is stale/used — start a fresh login, do not reuse the old URL or code.
- Ensure the whole flow (authorize URL → callback → exchange) happens in one run with no restarts in between.
- On 429, wait before retrying login.
- On 5xx, check the provider status page and retry later.
- If persistent, verify ClientID/TokenURL constants and redirect URI wiring are unmodified.
Defensive patterns
Strategy: validation
Validate before calling
// Never reuse a code; check it looks fresh before exchanging
if code == "" || len(code) < 16 {
return errors.New("authorization code missing or truncated; restart login")
} Try / catch
tok, err := auth.ExchangeCode(ctx, code)
if err != nil {
msg := err.Error()
switch {
case strings.Contains(msg, "invalid_grant"), strings.Contains(msg, "status 400"):
// code expired/used: MUST start a brand-new login, do not retry exchange
case strings.Contains(msg, "status 429"):
// rate limited: back off before next login attempt
case strings.Contains(msg, "status 5"):
// provider incident: retry later
}
} Prevention
- Complete authorize→callback→exchange in a single uninterrupted flow.
- Never copy/paste authorization codes between attempts.
- Rate-limit automated login scripts to avoid 429s.
When it happens
Trigger: Replaying an authorization code (codes are single-use); code expired before exchange; code_verifier regenerated between authorize and exchange steps; redirect_uri in the token request differs from the callback the code was issued for; 429 rate limiting from repeated login attempts; OpenAI-side incident returning 5xx.
Common situations: Restarting the login mid-flow and pasting an old code; clocks/edits causing verifier mismatch; hammering the login command in a loop; upstream OpenAI auth outage (check status.openai.com).
Related errors
- token refresh failed with status %d: %s
- failed to generate code verifier: %w
- port %d is already in use
- timeout waiting for OAuth callback
- failed to create token request: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/e95744ab1879e2d1.
Report an issue: GitHub.