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

  1. 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.
  2. Ensure the whole flow (authorize URL → callback → exchange) happens in one run with no restarts in between.
  3. On 429, wait before retrying login.
  4. On 5xx, check the provider status page and retry later.
  5. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/e95744ab1879e2d1. Report an issue: GitHub.