jlcodes99/cockpit-tools · error

Token 解析失败: {}

Error message

Token 解析失败: {}

What it means

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.

Source

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

    let response = client
        .post(TOKEN_URL)
        .form(&params)
        .send()
        .await
        .map_err(|e| {
            let msg = format!("Token 交换请求失败: {}", e);
            crate::modules::logger::log_error(&msg);
            msg
        })?;

    let status = response.status();
    crate::modules::logger::log_info(&format!("Token 交换响应状态: {}", status));

    if status.is_success() {
        let mut token_res = response.json::<TokenResponse>().await.map_err(|e| {
            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)

View on GitHub (pinned to 1ed8b77992)

Solutions

  1. Inspect the wrapped serde message to see which expected field/type mismatched, then capture the raw body for comparison.
  2. Check whether a proxy or captive portal returned HTML instead of JSON (curl the endpoint from the same machine).
  3. Update Cockpit Tools if the provider changed the token response schema; report a schema mismatch otherwise.
  4. Retry the login from a network without TLS/HTTP interception.
  5. If a required field (e.g. access_token/refresh_token) is missing, verify the client_id/scopes sent during authorization.

Example fix

// before: assuming 2xx means valid token JSON
let tokens = exchange_code(&code, &verifier).await?;
// after: distinguish parse failure and log the raw body
match exchange_code(&code, &verifier).await {
    Err(e) if e.contains("Token 解析失败") => {
        log_raw_token_response_for_debug(); // inspect actual payload
        ui.show("登录响应格式异常,请检查网络代理后重试");
    }
    r => handle(r),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Detect non-JSON (HTML/proxy) 200 responses by shape before trusting tokens
fn looks_like_html(body: &str) -> bool {
    let b = body.trim_start();
    b.starts_with('<') || b.to_ascii_lowercase().contains("<html")
}

Type guard

fn is_token_parse_failure(err: &str) -> bool {
    err.starts_with("Token 解析失败: ")
}

Try / catch

match exchange_code(&code, &verifier).await {
    Err(e) if is_token_parse_failure(&e) => {
        log::error!("token response not parseable: {} — check proxy/captive portal", e);
        ui.show("登录响应异常,请关闭代理/验证网络后重试");
    }
    other => handle(other),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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