Kuberwastaken/claurst · error
Token exchange failed
Error message
Token exchange failed ({}): {} What it means
Raised in exchange_code_for_tokens when the POST that swaps the OAuth authorization code for access/refresh tokens returns a non-success HTTP status. The error embeds the status code and the raw response body so the developer can see the OAuth server's rejection reason (invalid_grant, invalid_client, etc.).
Solutions
- Read the embedded status and body — an 'invalid_grant' body means the code expired or was already used; simply restart the login flow.
- Verify client_id/client_secret configuration matches the OAuth app registered with the provider.
- Confirm the redirect_uri sent to the token endpoint is byte-identical to the one in the authorization URL (scheme, host, port).
- Restart the login promptly after authorizing — authorization codes expire within minutes and are single-use.
- Check the token endpoint base URL for recent provider changes or version bumps.
Example fix
// before: blind retry re-posts an already-consumed code and fails again
let resp = client.post(&token_url).form(¶ms).send().await?;
// after: surface a distinct message for invalid_grant so users know to re-login
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if text.contains("invalid_grant") {
bail!("Authorization code expired or already used — please restart the login flow");
}
bail!("Token exchange failed ({}): {}", status, text);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate code freshness and redirect_uri consistency before exchanging
let code_age = started_at.elapsed()?;
if code_age > Duration::from_secs(300) {
bail!("authorization code likely expired — restart login");
}
assert_eq!(redirect_uri_used_in_auth_url, redirect_uri_sent_to_token_endpoint); Type guard
fn is_retryable_exchange_failure(status: u16, body: &str) -> bool {
status >= 500 || status == 429
|| !(body.contains("invalid_grant") || body.contains("invalid_client"))
} Try / catch
match exchange_code_for_tokens(&code, &verifier).await {
Ok(tokens) => tokens,
Err(e) if e.to_string().contains("invalid_grant") => {
eprintln!("Authorization code expired or already used — restarting login...");
restart_login_flow().await?
}
Err(e) => return Err(e),
} Prevention
- Exchange the code immediately after the callback — codes are single-use and short-lived
- Never retry a failed exchange with the same code
- Keep redirect_uri identical between the authorization URL and the token request
- Verify client_id/client_secret after any provider-side credential rotation
When it happens
Trigger: exchange_code_for_tokens receives resp with !resp.status().is_success() from the token endpoint: the authorization code was already consumed or expired (single-use, ~minutes lifetime), the client_id/client_secret or PKCE verifier is wrong, the redirect_uri does not exactly match the one used in the authorization request, or the endpoint is down/misconfigured (404/500).
Common situations: Developers hit this when the user takes too long to authorize (code expired), when the code was already exchanged by a retry after a timeout, when env config for client credentials is stale after a provider-side client rotation, or when the token endpoint URL changed with an API version bump.
Related errors
- Token exchange failed
- exchange_code: HTTP
- Bridge register: server returned
- API key creation failed
- refresh: HTTP
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/5f39a3942536e9aa.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/cli/src/oauth_flow.rs:408
"state": state,
});
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
let resp = client
.post(oauth::TOKEN_URL)
.header("content-type", "application/json")
.json(&body)
.send()
.await
.context("Token exchange HTTP request failed")?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
bail!("Token exchange failed ({}): {}", status, text);
}
resp.json::<TokenExchangeResponse>()
.await
.context("Failed to parse token exchange response")
}
/// Exchange an OAuth access token for an Anthropic API key (Console flow only).
async fn create_api_key(access_token: &str) -> anyhow::Result<String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
let resp = client
.post(oauth::API_KEY_URL)
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await
View on GitHub (pinned to b0637c97ec)