{"record":{"id":"5f39a3942536e9aa","repo":"Kuberwastaken/claurst","slug":"token-exchange-failed-5f39a3","errorCode":null,"errorMessage":"Token exchange failed ({}): {}","messagePattern":"Token exchange failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-rust/crates/cli/src/oauth_flow.rs","lineNumber":408,"sourceCode":"        \"state\": state,\r\n    });\r\n\r\n    let client = reqwest::Client::builder()\r\n        .timeout(Duration::from_secs(30))\r\n        .build()?;\r\n\r\n    let resp = client\r\n        .post(oauth::TOKEN_URL)\r\n        .header(\"content-type\", \"application/json\")\r\n        .json(&body)\r\n        .send()\r\n        .await\r\n        .context(\"Token exchange HTTP request failed\")?;\r\n\r\n    if !resp.status().is_success() {\r\n        let status = resp.status();\r\n        let text = resp.text().await.unwrap_or_default();\r\n        bail!(\"Token exchange failed ({}): {}\", status, text);\r\n    }\r\n\r\n    resp.json::<TokenExchangeResponse>()\r\n        .await\r\n        .context(\"Failed to parse token exchange response\")\r\n}\r\n\r\n/// Exchange an OAuth access token for an Anthropic API key (Console flow only).\r\nasync fn create_api_key(access_token: &str) -> anyhow::Result<String> {\r\n    let client = reqwest::Client::builder()\r\n        .timeout(Duration::from_secs(30))\r\n        .build()?;\r\n\r\n    let resp = client\r\n        .post(oauth::API_KEY_URL)\r\n        .header(\"Authorization\", format!(\"Bearer {}\", access_token))\r\n        .send()\r\n        .await\r","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/Kuberwastaken/claurst/blob/b0637c97ec34144387cbf2f74f65df6d16a6cef1/src-rust/crates/cli/src/oauth_flow.rs#L390-L426","documentation":"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.).","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: blind retry re-posts an already-consumed code and fails again\nlet resp = client.post(&token_url).form(&params).send().await?;\n// after: surface a distinct message for invalid_grant so users know to re-login\nif !resp.status().is_success() {\n    let status = resp.status();\n    let text = resp.text().await.unwrap_or_default();\n    if text.contains(\"invalid_grant\") {\n        bail!(\"Authorization code expired or already used — please restart the login flow\");\n    }\n    bail!(\"Token exchange failed ({}): {}\", status, text);\n}","handlingStrategy":"try-catch","validationCode":"// Validate code freshness and redirect_uri consistency before exchanging\nlet code_age = started_at.elapsed()?;\nif code_age > Duration::from_secs(300) {\n    bail!(\"authorization code likely expired — restart login\");\n}\nassert_eq!(redirect_uri_used_in_auth_url, redirect_uri_sent_to_token_endpoint);","typeGuard":"fn is_retryable_exchange_failure(status: u16, body: &str) -> bool {\n    status >= 500 || status == 429\n        || !(body.contains(\"invalid_grant\") || body.contains(\"invalid_client\"))\n}","tryCatchPattern":"match exchange_code_for_tokens(&code, &verifier).await {\n    Ok(tokens) => tokens,\n    Err(e) if e.to_string().contains(\"invalid_grant\") => {\n        eprintln!(\"Authorization code expired or already used — restarting login...\");\n        restart_login_flow().await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"tags":["oauth","http","token-exchange","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"b0637c97ec34144387cbf2f74f65df6d16a6cef1","analyzedAt":"2026-09-10T00:24:58.650Z","contentChangedAt":"2026-09-10T00:24:58.650Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}