jlcodes99/cockpit-tools · error
Token 刷新失败: status={}
Error message
Token 刷新失败: status={} What it means
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.
Source
Thrown at crates/cockpit-core/src/modules/codex_oauth.rs:1208
let response = apply_codex_auth_identity_headers(client.post(TOKEN_ENDPOINT))
.json(&serde_json::json!({
"client_id": CLIENT_ID,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}))
.send()
.await
.map_err(|e| format!("Token 刷新请求失败: {}", e))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|e| format!("读取响应失败: {}", e))?;
if !status.is_success() {
let error_code = extract_token_error_code(&body);
logger::log_error(&format!(
"Token 刷新失败: status={}, error_code={:?}, body_len={}",
status,
error_code,
body.len()
));
let mut message = format!("Token 刷新失败: status={}", status);
if let Some(code) = error_code {
message.push_str(&format!(", error_code={}", code));
}
message.push_str(&format!(", body_len={}", body.len()));
return Err(message);
}
logger::log_info("Codex Token 刷新成功");
let token_response: serde_json::Value =
serde_json::from_str(&body).map_err(|e| format!("解析 Token 响应失败: {}", e))?;
View on GitHub (pinned to 1ed8b77992)
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.
Example fix
// before: silently retrying refresh on any failure
if let Err(_) = refresh_access_token(&token.refresh_token).await { /* retry loop */ }
// after: distinguish fatal invalid_grant from transient failures
match refresh_access_token(&token.refresh_token).await {
Err(msg) if msg.contains("400") || msg.contains("invalid_grant") => {
// refresh token is revoked: force full re-login
require_relogin(account_id);
}
Err(msg) => schedule_retry_with_backoff(account_id, msg),
Ok(tokens) => persist_tokens(tokens),
} Defensive patterns
Strategy: fallback
Validate before calling
// Pre-check the refresh token before calling the API
fn refresh_token_plausible(t: &str) -> bool {
let t = t.trim();
!t.is_empty() && t.len() > 20 && t.chars().all(|c| !c.is_whitespace())
}
if !refresh_token_plausible(&token.refresh_token) {
return require_relogin(account_id);
} Type guard
fn is_fatal_refresh_rejection(err: &str) -> bool {
err.contains("400") || err.contains("401") || err.contains("invalid_grant")
} Try / catch
match refresh_access_token(&rt).await {
Err(msg) if is_fatal_refresh_rejection(&msg) => force_relogin(account_id),
Err(msg) => retry_with_backoff(account_id, msg),
Ok(tokens) => persist(tokens),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of jlcodes99/cockpit-tools@1ed8b77992 (2026-09-05).
Data as JSON: /api/errors/e40bac20fecdd6f8.
Report an issue: GitHub.