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

  1. 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.
  2. If the token was rotated, ensure only the latest refresh_token is persisted and never reuse an older one after a successful refresh.
  3. For 429/5xx, retry with exponential backoff after a delay instead of immediately re-authenticating.
  4. Verify network connectivity and that no proxy intercepts requests to the Codex TOKEN_ENDPOINT.
  5. 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

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.