{"record":{"id":"a75a92826743cabf","repo":"jlcodes99/cockpit-tools","slug":"token-a75a92","errorCode":null,"errorMessage":"Token 解析失败: {}","messagePattern":"Token 解析失败: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/cockpit-core/src/modules/oauth.rs","lineNumber":136,"sourceCode":"\n    let response = client\n        .post(TOKEN_URL)\n        .form(&params)\n        .send()\n        .await\n        .map_err(|e| {\n            let msg = format!(\"Token 交换请求失败: {}\", e);\n            crate::modules::logger::log_error(&msg);\n            msg\n        })?;\n\n    let status = response.status();\n    crate::modules::logger::log_info(&format!(\"Token 交换响应状态: {}\", status));\n\n    if status.is_success() {\n        let mut token_res = response.json::<TokenResponse>().await.map_err(|e| {\n            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)","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/jlcodes99/cockpit-tools/blob/1ed8b77992d62ca81fabf744deb0839ad361d5bf/crates/cockpit-core/src/modules/oauth.rs#L118-L154","documentation":"Returned by oauth::exchange_code when the token endpoint answered with a success HTTP status but the response body could not be deserialized into TokenResponse (response.json::<TokenResponse>() failed). The serde error is wrapped as \"Token 解析失败: {}\" and propagated, so the authorization code exchange yields no tokens.","triggerScenarios":"Calling exchange_code when the server returns 2xx with a body that is not the expected TokenResponse JSON: an HTML error/captive-portal page with status 200, missing required fields, changed API schema, or a proxy returning a 200 interstitial.","commonSituations":"Captive portal or proxy injecting HTML with 200 status; provider changed the token response shape (removed/renamed fields) after a client/server version mismatch; CDN/WAF challenge page; truncated response body.","solutions":["Inspect the wrapped serde message to see which expected field/type mismatched, then capture the raw body for comparison.","Check whether a proxy or captive portal returned HTML instead of JSON (curl the endpoint from the same machine).","Update Cockpit Tools if the provider changed the token response schema; report a schema mismatch otherwise.","Retry the login from a network without TLS/HTTP interception.","If a required field (e.g. access_token/refresh_token) is missing, verify the client_id/scopes sent during authorization."],"exampleFix":"// before: assuming 2xx means valid token JSON\nlet tokens = exchange_code(&code, &verifier).await?;\n// after: distinguish parse failure and log the raw body\nmatch exchange_code(&code, &verifier).await {\n    Err(e) if e.contains(\"Token 解析失败\") => {\n        log_raw_token_response_for_debug(); // inspect actual payload\n        ui.show(\"登录响应格式异常，请检查网络代理后重试\");\n    }\n    r => handle(r),\n}","handlingStrategy":"try-catch","validationCode":"// Detect non-JSON (HTML/proxy) 200 responses by shape before trusting tokens\nfn looks_like_html(body: &str) -> bool {\n    let b = body.trim_start();\n    b.starts_with('<') || b.to_ascii_lowercase().contains(\"<html\")\n}","typeGuard":"fn is_token_parse_failure(err: &str) -> bool {\n    err.starts_with(\"Token 解析失败: \")\n}","tryCatchPattern":"match exchange_code(&code, &verifier).await {\n    Err(e) if is_token_parse_failure(&e) => {\n        log::error!(\"token response not parseable: {} — check proxy/captive portal\", e);\n        ui.show(\"登录响应异常，请关闭代理/验证网络后重试\");\n    }\n    other => handle(other),\n}","preventionTips":["Avoid networks with captive portals or HTTP-intercepting proxies during login.","Keep the app updated so TokenResponse matches the provider's current schema.","When this fires, capture the raw body (curl the endpoint) to compare with the expected fields.","Check for CDN/WAF challenge pages returning 200 with HTML.","Verify required fields (access_token/refresh_token) exist in provider responses after provider updates."],"tags":["oauth","json","deserialization","schema-mismatch"],"backgroundTag":"json-deserialization-failed","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"}