{"record":{"id":"e40bac20fecdd6f8","repo":"jlcodes99/cockpit-tools","slug":"token-status","errorCode":null,"errorMessage":"Token 刷新失败: status={}","messagePattern":"Token 刷新失败: status=(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/cockpit-core/src/modules/codex_oauth.rs","lineNumber":1208,"sourceCode":"    let response = apply_codex_auth_identity_headers(client.post(TOKEN_ENDPOINT))\n        .json(&serde_json::json!({\n            \"client_id\": CLIENT_ID,\n            \"grant_type\": \"refresh_token\",\n            \"refresh_token\": refresh_token,\n        }))\n        .send()\n        .await\n        .map_err(|e| format!(\"Token 刷新请求失败: {}\", e))?;\n\n    let status = response.status();\n    let body = response\n        .text()\n        .await\n        .map_err(|e| format!(\"读取响应失败: {}\", e))?;\n\n    if !status.is_success() {\n        let error_code = extract_token_error_code(&body);\n        logger::log_error(&format!(\n            \"Token 刷新失败: status={}, error_code={:?}, body_len={}\",\n            status,\n            error_code,\n            body.len()\n        ));\n        let mut message = format!(\"Token 刷新失败: status={}\", status);\n        if let Some(code) = error_code {\n            message.push_str(&format!(\", error_code={}\", code));\n        }\n        message.push_str(&format!(\", body_len={}\", body.len()));\n        return Err(message);\n    }\n\n    logger::log_info(\"Codex Token 刷新成功\");\n\n    let token_response: serde_json::Value =\n        serde_json::from_str(&body).map_err(|e| format!(\"解析 Token 响应失败: {}\", e))?;\n","sourceCodeStart":1190,"sourceCodeEnd":1226,"githubUrl":"https://github.com/jlcodes99/cockpit-tools/blob/1ed8b77992d62ca81fabf744deb0839ad361d5bf/crates/cockpit-core/src/modules/codex_oauth.rs#L1190-L1226","documentation":"Thrown by refresh_access_token_with_fallback when the Codex OAuth token endpoint returns a non-2xx HTTP status to a refresh_token grant request. The message carries the HTTP status, an optional OAuth error_code extracted from the response body (e.g. invalid_grant), and the body length, and is returned as an Err(String) instead of refreshed tokens. It means the stored refresh_token could not be exchanged for a new access_token.","triggerScenarios":"Calling refresh_access_token (or refresh_access_token_with_fallback) when the token endpoint responds 400 invalid_grant (refresh token revoked/expired/rotated), 401 (client auth rejected), 429 (rate limited), or 5xx from the auth server.","commonSituations":"User logged out and revoked sessions elsewhere; refresh token was already used and rotated (replay of an old token); long-lived offline install where the refresh token expired; clock skew; provider-side outage returning 5xx; proxy/firewall mangling the request.","solutions":["Check the status/error_code in the message: if 400/invalid_grant, the refresh token is dead — re-authenticate the account via a fresh Codex OAuth login.","If the token was rotated, ensure only the latest refresh_token is persisted and never reuse an older one after a successful refresh.","For 429/5xx, retry with exponential backoff after a delay instead of immediately re-authenticating.","Verify network connectivity and that no proxy intercepts requests to the Codex TOKEN_ENDPOINT.","If it persists, remove the stored account credentials and log in again to obtain fresh refresh/id tokens."],"exampleFix":"// before: silently retrying refresh on any failure\nif let Err(_) = refresh_access_token(&token.refresh_token).await { /* retry loop */ }\n// after: distinguish fatal invalid_grant from transient failures\nmatch refresh_access_token(&token.refresh_token).await {\n    Err(msg) if msg.contains(\"400\") || msg.contains(\"invalid_grant\") => {\n        // refresh token is revoked: force full re-login\n        require_relogin(account_id);\n    }\n    Err(msg) => schedule_retry_with_backoff(account_id, msg),\n    Ok(tokens) => persist_tokens(tokens),\n}","handlingStrategy":"fallback","validationCode":"// Pre-check the refresh token before calling the API\nfn refresh_token_plausible(t: &str) -> bool {\n    let t = t.trim();\n    !t.is_empty() && t.len() > 20 && t.chars().all(|c| !c.is_whitespace())\n}\nif !refresh_token_plausible(&token.refresh_token) {\n    return require_relogin(account_id);\n}","typeGuard":"fn is_fatal_refresh_rejection(err: &str) -> bool {\n    err.contains(\"400\") || err.contains(\"401\") || err.contains(\"invalid_grant\")\n}","tryCatchPattern":"match refresh_access_token(&rt).await {\n    Err(msg) if is_fatal_refresh_rejection(&msg) => force_relogin(account_id),\n    Err(msg) => retry_with_backoff(account_id, msg),\n    Ok(tokens) => persist(tokens),\n}","preventionTips":["Always persist the newest refresh_token after each successful refresh (tokens rotate).","Never run two refresh flows concurrently for the same account.","Treat 400/invalid_grant as terminal and trigger re-login instead of retrying.","Use backoff with jitter for 429/5xx responses.","Log error_code and body_len from the message when diagnosing."],"tags":["oauth","token-refresh","network","http-status"],"backgroundTag":"oauth-refresh-token-rejected","analyzedSha":"1ed8b77992d62ca81fabf744deb0839ad361d5bf","analyzedAt":"2026-09-05T09:51:41.178Z","contentChangedAt":"2026-09-05T09:51:41.178Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}