{"record":{"id":"61d6128b066044ed","repo":"jlcodes99/cockpit-tools","slug":"token-body-len","errorCode":null,"errorMessage":"Token 交换失败 ({})，body_len={}","messagePattern":"Token 交换失败 \\((.+?)\\)，body_len=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/cockpit-core/src/modules/oauth.rs","lineNumber":153,"sourceCode":"            let msg = format!(\"Token 解析失败: {}\", e);\n            crate::modules::logger::log_error(&msg);\n            msg\n        })?;\n        token_res.oauth_client_key = Some(client_key);\n\n        if token_res.refresh_token.is_some() {\n            crate::modules::logger::log_info(\"Token 交换成功, 获取到 refresh_token\");\n        } else {\n            crate::modules::logger::log_warn(\n                \"警告: Google 未返回 refresh_token, 可能之前已授权过此应用\",\n            );\n        }\n\n        Ok(token_res)\n    } else {\n        let error_text = response.text().await.unwrap_or_default();\n        let msg = format!(\"Token 交换失败 ({})，body_len={}\", status, error_text.len());\n        crate::modules::logger::log_error(&msg);\n        Err(msg)\n    }\n}\n\n/// 使用 refresh_token 刷新 access_token\npub async fn refresh_access_token(refresh_token: &str) -> Result<TokenResponse, String> {\n    refresh_access_token_with_client(refresh_token, None).await\n}\n\n/// 使用指定 OAuth client 刷新 access_token。\npub async fn refresh_access_token_with_client(\n    refresh_token: &str,\n    oauth_client_key: Option<&str>,\n) -> Result<TokenResponse, String> {\n    let client = crate::utils::http::create_client(15);\n    let (client_id, client_secret, client_key) = oauth_client_config(oauth_client_key)?;\n\n    let params = [","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/jlcodes99/cockpit-tools/blob/1ed8b77992d62ca81fabf744deb0839ad361d5bf/crates/cockpit-core/src/modules/oauth.rs#L135-L171","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: retrying exchange with the same code\nfor _ in 0..3 { if let Ok(t) = exchange_code(&code, &v).await { break; } }\n// after: a failed exchange invalidates the code — restart the flow\nmatch exchange_code(&code, &verifier).await {\n    Err(e) if e.contains(\"Token 交换失败 (400\") => restart_oauth_login(), // fresh code\n    Err(e) => ui.show(e),\n    Ok(t) => save(t),\n}","handlingStrategy":"fallback","validationCode":"// Don't call exchange twice with the same code; guard with a consumed flag\nstruct CodeOnce { code: String, used: std::cell::Cell<bool> }\nimpl CodeOnce {\n    fn try_exchange(&self, verifier: &str) -> Option<Result<TokenResponse, String>> {\n        if self.used.get() { return None; } // code already consumed — restart login instead\n        self.used.set(true);\n        Some(pollster::block_on(exchange_code(&self.code, verifier)))\n    }\n}","typeGuard":"fn is_exchange_rejection(err: &str) -> bool {\n    err.starts_with(\"Token 交换失败 (\")\n}\nfn is_code_expired(err: &str) -> bool { err.contains(\"(400\") || err.contains(\"(401\") }","tryCatchPattern":"match exchange_code(&code, &verifier).await {\n    Err(e) if is_exchange_rejection(&e) && is_code_expired(&e) => {\n        restart_oauth_login() // fresh code; old one is burned\n    }\n    Err(e) => ui.show(e),\n    Ok(tokens) => save(tokens),\n}","preventionTips":["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."],"tags":["oauth","authorization-code","http-status","pkce"],"backgroundTag":"oauth-code-exchange-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"}