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
- 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.
- Ensure the code is exchanged exactly once (avoid double-submit from UI retries).
- Verify the PKCE code_verifier and redirect_uri match exactly what was used in the authorization request.
- Check the configured OAuth client key/secret against the current provider preset.
- 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
- Exchange the authorization code immediately after receiving it — codes are short-lived.
- Never retry exchange with the same code; a failed/consumed code requires a new authorization.
- Ensure PKCE verifier and redirect_uri are byte-identical to the authorization request.
- Validate client_id/client_key configuration against the current provider preset.
- Treat 5xx as transient: restart login after a short wait.
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
- Token 刷新失败: status={}
- Token 交换失败: status={}, body_len={}
- Claude login start 响应缺少关键字段
- Claude OAuth start 响应缺少关键字段
- Qoder OAuth start 响应缺少关键字段
AI-assisted analysis of jlcodes99/cockpit-tools@1ed8b77992 (2026-09-05).
Data as JSON: /api/errors/61d6128b066044ed.
Report an issue: GitHub.