{"record":{"id":"f9b54f1381c912db","repo":"Kuberwastaken/claurst","slug":"token-refresh-failed","errorCode":null,"errorMessage":"Token refresh failed ({}): {}","messagePattern":"Token refresh failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-rust/crates/cli/src/oauth_flow.rs","lineNumber":472,"sourceCode":"        \"scope\": oauth::ALL_SCOPES.join(\" \"),\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 refresh 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 refresh failed ({}): {}\", status, text);\r\n    }\r\n\r\n    let token_resp: TokenExchangeResponse = resp.json().await?;\r\n    let expires_at_ms = chrono::Utc::now().timestamp_millis()\r\n        + (token_resp.expires_in as i64 * 1000);\r\n\r\n    let scopes: Vec<String> = token_resp\r\n        .scope\r\n        .as_deref()\r\n        .unwrap_or(\"\")\r\n        .split_whitespace()\r\n        .map(String::from)\r\n        .collect();\r\n\r\n    let mut updated = tokens.clone();\r\n    updated.access_token = token_resp.access_token;\r\n    if let Some(new_rt) = token_resp.refresh_token {\r\n        updated.refresh_token = Some(new_rt);\r","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/Kuberwastaken/claurst/blob/b0637c97ec34144387cbf2f74f65df6d16a6cef1/src-rust/crates/cli/src/oauth_flow.rs#L454-L490","documentation":"Raised in the public refresh_oauth_token when the refresh-token POST to the OAuth token endpoint returns a non-success HTTP status. The HTTP status and response body (typically containing grant_type/invalid_grant details from the OAuth server) are embedded in the message. Callers rely on this to silently refresh expired access tokens, so this error surfaces when the stored refresh credential is no longer acceptable.","triggerScenarios":"refresh_oauth_token sends the stored refresh_token with grant_type=refresh_token and receives !resp.status().is_success(): the refresh token was revoked, rotated (single-use refresh tokens — an old one was replayed), expired after provider inactivity windows, or the client credentials/endpoint config is wrong.","commonSituations":"Developers hit this after switching machines or logging in elsewhere (revoking the old session), after clock/restore scenarios that invalidate tokens, when multiple processes refresh concurrently and race the single-use rotation, or after the provider shortens refresh-token lifetimes.","solutions":["Treat this as 'session fully expired': re-run the OAuth login flow to obtain fresh access and refresh tokens.","If refresh tokens are single-use, ensure only one process refreshes at a time and the new refresh token is persisted atomically after each refresh.","Check the embedded status/body: 400 invalid_grant means revocation/expiry — no retry will help; 5xx is transient and worth retrying with backoff.","Verify client_id/client_secret and the token endpoint URL are current.","Confirm the system clock is correct — large skew can invalidate token validation."],"exampleFix":"// before: any failure aborts with a raw HTTP message\nif !resp.status().is_success() {\n    bail!(\"Token refresh failed ({}): {}\", resp.status(), resp.text().await.unwrap_or_default());\n}\n// after: distinguish transient from permanent failures for callers\nlet status = resp.status();\nif status.is_server_error() {\n    bail!(\"Token refresh temporarily failed ({}) — retry later\", status);\n}\nif !status.is_success() {\n    let text = resp.text().await.unwrap_or_default();\n    if text.contains(\"invalid_grant\") {\n        bail!(\"Refresh token revoked or expired — please log in again\");\n    }\n    bail!(\"Token refresh failed ({}): {}\", status, text);\n}","handlingStrategy":"fallback","validationCode":"// Before refreshing, confirm a refresh token exists and is plausibly unexpired\nfn can_refresh(tokens: &OAuthTokens) -> bool {\n    tokens.refresh_token.as_deref().map_or(false, |t| !t.is_empty())\n}","typeGuard":"fn is_permanent_refresh_failure(body: &str) -> bool {\n    // invalid_grant = revoked/rotated/expired — re-login required\n    body.contains(\"invalid_grant\") || body.contains(\"invalid_client\")\n}","tryCatchPattern":"match refresh_oauth_token(&refresh_token).await {\n    Ok(new_tokens) => new_tokens,\n    Err(e) if e.to_string().contains(\"invalid_grant\") => {\n        eprintln!(\"Session expired — starting interactive login...\");\n        run_oauth_login_flow().await?\n    }\n    Err(e) if is_transient(&e) => {\n        backoff_retry(|| refresh_oauth_token(&refresh_token), 3).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Persist rotated single-use refresh tokens atomically immediately after each refresh","Serialize refreshes across processes with a lock to avoid racing the rotation","Refresh proactively before access-token expiry instead of on 401","Fall back to interactive login when the refresh token is permanently rejected","Verify system clock accuracy on hosts performing refreshes"],"tags":["oauth","token-refresh","http","authentication"],"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"}