jlcodes99/cockpit-tools · error

Token 交换请求失败

Error message

Token 交换请求失败: {}

What it means

Returned by oauth::exchange_code when the HTTP POST to the OAuth TOKEN_URL itself fails (reqwest .send() error: DNS failure, connection refused/reset, TLS error, timeout). The reqwest Display error is wrapped as "Token 交换请求失败: {}" and propagated as Err(String), aborting the authorization-code exchange.

Solutions

  1. Check the wrapped reqwest message for the concrete cause (dns error, connection refused, timeout) and fix connectivity first.
  2. Verify proxy environment variables (HTTP_PROXY/HTTPS_PROXY) are correct or unset as appropriate.
  3. Retry the exchange — authorization codes are short-lived, so restart the login flow if the code expired.
  4. Confirm the auth endpoint domain is reachable (`curl -v $TOKEN_URL`) and not blocked by firewall/VPN policy.
  5. If TLS interception is the cause, trust the corporate CA or bypass inspection for the auth host.

Example fix

// before: surfacing the raw error to the user
match exchange_code(&code, &verifier).await {
    Err(e) => ui.show(e),
    Ok(t) => save(t),
}
// after: classify network failure and offer retry
match exchange_code(&code, &verifier).await {
    Err(e) if e.contains("交换请求失败") => ui.show_retryable("网络错误,请检查连接后重试", e),
    Err(e) => ui.show(e),
    Ok(t) => save(t),
}
Defensive patterns

Strategy: retry

Validate before calling

// Probe connectivity to the token endpoint before exchanging the code
async fn token_endpoint_reachable(url: &str) -> bool {
    reqwest::Client::new().get(url).timeout(std::time::Duration::from_secs(5))
        .send().await.map(|r| r.status().as_u16() < 500).unwrap_or(false)
}

Type guard

fn is_network_transport_error(err: &str) -> bool {
    err.starts_with("Token 交换请求失败: ")
}

Try / catch

match exchange_code(&code, &verifier).await {
    Err(e) if is_network_transport_error(&e) => {
        if backoff_retry(|| exchange_code(&code, &verifier), 3).is_err() {
            ui.show("网络异常,请检查连接/代理后重试登录");
        }
    }
    other => handle(other),
}

Prevention

When it happens

Trigger: Calling exchange_code (authorization code -> token) when the network request to TOKEN_URL cannot complete: offline machine, unreachable/blocked endpoint, DNS failure, TLS interception, proxy misconfiguration, or request timeout.

Common situations: Corporate proxy or firewall blocking the auth domain; VPN required but not connected; SSL-inspecting middlebox breaking TLS; transient network outage right after pasting the auth code; system clock issues causing TLS handshake failure.

Related errors


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

Appendix: source

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

    let client = crate::utils::http::create_client(15);
    let (client_id, client_secret, client_key) = oauth_client_config(None)?;

    let params = [
        ("client_id", client_id),
        ("client_secret", client_secret),
        ("code", code),
        ("redirect_uri", redirect_uri),
        ("grant_type", "authorization_code"),
    ];

    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(

View on GitHub (pinned to 1ed8b77992)