Kuberwastaken/claurst · error · anyhow::Error

Token exchange failed

Error message

Token exchange failed ({}): {}

What it means

After receiving the OAuth authorization code, exchange_code_for_tokens POSTs it (with client_id, PKCE code_verifier, grant_type, and redirect_uri) to the Codex token endpoint. This error is raised when the token endpoint returns a non-2xx HTTP status; the message embeds the status code and the response body so the developer can see the provider's rejection reason (e.g. invalid_grant, invalid_client).

Solutions

  1. Read the embedded status and body in the message: `invalid_grant` means the code is expired or already used — restart the OAuth flow to get a fresh code
  2. Retry the full login flow (`claurst auth login`) immediately and complete the browser step without long delays
  3. Verify network access to the token endpoint (no proxy/VPN interference); test with `curl -X POST <CODEX_TOKEN_URL>`
  4. If it persists, check for a claurst/version mismatch with the provider's OAuth client (client_id/redirect_uri changes) and update claurst
  5. Check system clock accuracy (NTP) — skew can break token validation server-side

Example fix

// before: re-running the exchange with a stale, already-consumed code
exchange_code_for_tokens("old_used_code", &verifier).await
// -> Token exchange failed (400 Bad Request): {"error":"invalid_grant"...}

// after: start a fresh flow so a new code+verifier pair is issued
run_oauth_flow_with_label("Codex").await?;
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm the token endpoint is reachable before the flow
let ok = reqwest::get(CODEX_TOKEN_URL.replace("/token", "/.well-known/openid-configuration"))
    .await.map(|r| r.status().is_success()).unwrap_or(false);
anymore::ensure!(ok, "token endpoint unreachable — check network/proxy");

Try / catch

match run_oauth_flow_with_label("Codex").await {
    Err(e) if e.to_string().contains("Token exchange failed") => {
        // invalid_grant: code expired/used — restart the whole flow once
        run_oauth_flow_with_label("Codex").await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: reqwest receives a successful transport response but `resp.status().is_success()` is false for the POST to CODEX_TOKEN_URL. Typical provider responses: 400 invalid_grant (code already used or expired), 400 invalid_grant (PKCE code_verifier mismatch), 401 invalid_client (wrong CODEX_CLIENT_ID), or 5xx from the auth service.

Common situations: Replaying an authorization code that was already exchanged (codes are single-use); the callback took longer than the code's ~few-minute lifetime; a proxy/corporate network intercepts the POST; clock skew invalidating PKCE/token validation; the provider rotated client credentials between the auth request and the exchange.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/8704a8743dffe64c. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/cli/src/codex_oauth_flow.rs:205

    let params = [
        ("client_id", CODEX_CLIENT_ID),
        ("code", code),
        ("code_verifier", verifier),
        ("grant_type", "authorization_code"),
        ("redirect_uri", CODEX_REDIRECT_URI),
    ];

    let resp = client
        .post(CODEX_TOKEN_URL)
        .form(&params)
        .send()
        .await
        .map_err(|e| anyhow!("Failed to exchange code: {}", e))?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        bail!("Token exchange failed ({}): {}", status, body);
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| anyhow!("Failed to parse token response: {}", e))?;

    let access_token = body["access_token"]
        .as_str()
        .unwrap_or("")
        .to_string();

    if access_token.is_empty() {
        bail!("No access_token in response");
    }

    let refresh_token = body["refresh_token"].as_str().map(|s| s.to_string());
    let account_id = extract_account_id_from_jwt(&access_token);

View on GitHub (pinned to b0637c97ec)