{"record":{"id":"8704a8743dffe64c","repo":"Kuberwastaken/claurst","slug":"token-exchange-failed","errorCode":null,"errorMessage":"Token exchange failed ({}): {}","messagePattern":"Token exchange failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-rust/crates/cli/src/codex_oauth_flow.rs","lineNumber":205,"sourceCode":"    let params = [\r\n        (\"client_id\", CODEX_CLIENT_ID),\r\n        (\"code\", code),\r\n        (\"code_verifier\", verifier),\r\n        (\"grant_type\", \"authorization_code\"),\r\n        (\"redirect_uri\", CODEX_REDIRECT_URI),\r\n    ];\r\n\r\n    let resp = client\r\n        .post(CODEX_TOKEN_URL)\r\n        .form(&params)\r\n        .send()\r\n        .await\r\n        .map_err(|e| anyhow!(\"Failed to exchange code: {}\", e))?;\r\n\r\n    if !resp.status().is_success() {\r\n        let status = resp.status();\r\n        let body = resp.text().await.unwrap_or_default();\r\n        bail!(\"Token exchange failed ({}): {}\", status, body);\r\n    }\r\n\r\n    let body: serde_json::Value = resp\r\n        .json()\r\n        .await\r\n        .map_err(|e| anyhow!(\"Failed to parse token response: {}\", e))?;\r\n\r\n    let access_token = body[\"access_token\"]\r\n        .as_str()\r\n        .unwrap_or(\"\")\r\n        .to_string();\r\n\r\n    if access_token.is_empty() {\r\n        bail!(\"No access_token in response\");\r\n    }\r\n\r\n    let refresh_token = body[\"refresh_token\"].as_str().map(|s| s.to_string());\r\n    let account_id = extract_account_id_from_jwt(&access_token);\r","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/Kuberwastaken/claurst/blob/b0637c97ec34144387cbf2f74f65df6d16a6cef1/src-rust/crates/cli/src/codex_oauth_flow.rs#L187-L223","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Retry the full login flow (`claurst auth login`) immediately and complete the browser step without long delays","Verify network access to the token endpoint (no proxy/VPN interference); test with `curl -X POST <CODEX_TOKEN_URL>`","If it persists, check for a claurst/version mismatch with the provider's OAuth client (client_id/redirect_uri changes) and update claurst","Check system clock accuracy (NTP) — skew can break token validation server-side"],"exampleFix":"// before: re-running the exchange with a stale, already-consumed code\nexchange_code_for_tokens(\"old_used_code\", &verifier).await\n// -> Token exchange failed (400 Bad Request): {\"error\":\"invalid_grant\"...}\n\n// after: start a fresh flow so a new code+verifier pair is issued\nrun_oauth_flow_with_label(\"Codex\").await?;","handlingStrategy":"retry","validationCode":"// Pre-flight: confirm the token endpoint is reachable before the flow\nlet ok = reqwest::get(CODEX_TOKEN_URL.replace(\"/token\", \"/.well-known/openid-configuration\"))\n    .await.map(|r| r.status().is_success()).unwrap_or(false);\nanymore::ensure!(ok, \"token endpoint unreachable — check network/proxy\");","typeGuard":null,"tryCatchPattern":"match run_oauth_flow_with_label(\"Codex\").await {\n    Err(e) if e.to_string().contains(\"Token exchange failed\") => {\n        // invalid_grant: code expired/used — restart the whole flow once\n        run_oauth_flow_with_label(\"Codex\").await?;\n    }\n    other => other?,\n}","preventionTips":["Exchange the code immediately after the callback — codes are single-use and short-lived","Never re-run the exchange with a previously used authorization code","Keep system clock synchronized (NTP)","Exclude the auth host from corporate proxies/TLS inspection"],"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"}