jlcodes99/cockpit-tools · error

Token 交换失败 ({}),body_len={}

Error message

Token 交换失败 ({}),body_len={}

What it means

Returned by oauth::exchange_code when the token endpoint responds with a non-success HTTP status. The response body is read and only its length is embedded: "Token 交换失败 ({status}),body_len={n}". This means the authorization code, client credentials, redirect_uri, or PKCE verifier was rejected by the server.

Source

Thrown at crates/cockpit-core/src/modules/oauth.rs:153

            let msg = format!("Token 解析失败: {}", e);
            crate::modules::logger::log_error(&msg);
            msg
        })?;
        token_res.oauth_client_key = Some(client_key);

        if token_res.refresh_token.is_some() {
            crate::modules::logger::log_info("Token 交换成功, 获取到 refresh_token");
        } else {
            crate::modules::logger::log_warn(
                "警告: Google 未返回 refresh_token, 可能之前已授权过此应用",
            );
        }

        Ok(token_res)
    } else {
        let error_text = response.text().await.unwrap_or_default();
        let msg = format!("Token 交换失败 ({}),body_len={}", status, error_text.len());
        crate::modules::logger::log_error(&msg);
        Err(msg)
    }
}

/// 使用 refresh_token 刷新 access_token
pub async fn refresh_access_token(refresh_token: &str) -> Result<TokenResponse, String> {
    refresh_access_token_with_client(refresh_token, None).await
}

/// 使用指定 OAuth client 刷新 access_token。
pub async fn refresh_access_token_with_client(
    refresh_token: &str,
    oauth_client_key: Option<&str>,
) -> Result<TokenResponse, String> {
    let client = crate::utils::http::create_client(15);
    let (client_id, client_secret, client_key) = oauth_client_config(oauth_client_key)?;

    let params = [

View on GitHub (pinned to 1ed8b77992)

Solutions

  1. Restart the whole login flow to get a fresh authorization code — codes are single-use and short-lived, so 400 invalid_grant cannot be fixed by retrying the same code.
  2. Ensure the code is exchanged exactly once (avoid double-submit from UI retries).
  3. Verify the PKCE code_verifier and redirect_uri match exactly what was used in the authorization request.
  4. Check the configured OAuth client key/secret against the current provider preset.
  5. For 5xx statuses, wait and retry with a new login attempt; check provider status.

Example fix

// before: retrying exchange with the same code
for _ in 0..3 { if let Ok(t) = exchange_code(&code, &v).await { break; } }
// after: a failed exchange invalidates the code — restart the flow
match exchange_code(&code, &verifier).await {
    Err(e) if e.contains("Token 交换失败 (400") => restart_oauth_login(), // fresh code
    Err(e) => ui.show(e),
    Ok(t) => save(t),
}
Defensive patterns

Strategy: fallback

Validate before calling

// Don't call exchange twice with the same code; guard with a consumed flag
struct CodeOnce { code: String, used: std::cell::Cell<bool> }
impl CodeOnce {
    fn try_exchange(&self, verifier: &str) -> Option<Result<TokenResponse, String>> {
        if self.used.get() { return None; } // code already consumed — restart login instead
        self.used.set(true);
        Some(pollster::block_on(exchange_code(&self.code, verifier)))
    }
}

Type guard

fn is_exchange_rejection(err: &str) -> bool {
    err.starts_with("Token 交换失败 (")
}
fn is_code_expired(err: &str) -> bool { err.contains("(400") || err.contains("(401") }

Try / catch

match exchange_code(&code, &verifier).await {
    Err(e) if is_exchange_rejection(&e) && is_code_expired(&e) => {
        restart_oauth_login() // fresh code; old one is burned
    }
    Err(e) => ui.show(e),
    Ok(tokens) => save(tokens),
}

Prevention

When it happens

Trigger: Calling exchange_code when the server returns 400 (invalid_grant: code expired/already used, PKCE verifier mismatch), 401 (bad client_id/secret), or 5xx — i.e. any unsuccessful status on the TOKEN_URL POST.

Common situations: User takes too long between getting the auth code and exchanging it (code expired); the code was already consumed by a previous attempt; PKCE code_verifier doesn't match the code_challenge; wrong client_key/client_id configured; server-side outage returning 5xx.

Related errors


AI-assisted analysis of jlcodes99/cockpit-tools@1ed8b77992 (2026-09-05). Data as JSON: /api/errors/61d6128b066044ed. Report an issue: GitHub.