zeroclaw-labs/zeroclaw · error · anyhow::Error

OAuth refresh failed (HTTP {status}): {detail}

Error message

OAuth refresh failed (HTTP {status}): {detail}

What it means

resolve_qwen_oauth_context reads cached Qwen OAuth credentials from ~/.qwen/oauth_creds.json (written by the upstream `qwen login` flow) and calls refresh_qwen_oauth_access_token when the access token is missing or expiring within 30 s. A non-2xx status from the token endpoint fails with the response's error_description/error text, or the raw body when those fields are absent. This aborts provider construction.

Source

Thrown at crates/zeroclaw-providers/src/lib.rs:335

            );
            anyhow::Error::msg(format!("OAuth refresh request failed: {error}"))
        })?;

    let status = response.status();
    let body = response
        .text()
        .unwrap_or_else(|_| "<failed to read Qwen OAuth response body>".to_string());

    let parsed = serde_json::from_str::<QwenOauthTokenResponse>(&body).ok();

    if !status.is_success() {
        let detail = parsed
            .as_ref()
            .and_then(|payload| payload.error_description.as_deref())
            .or_else(|| parsed.as_ref().and_then(|payload| payload.error.as_deref()))
            .filter(|msg| !msg.trim().is_empty())
            .unwrap_or(body.as_str());
        anyhow::bail!("OAuth refresh failed (HTTP {status}): {detail}");
    }

    let payload = parsed.ok_or_else(|| {
        ::zeroclaw_log::record!(
            ERROR,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                .with_attrs(::serde_json::json!({
                    "oauth_provider": "qwen",
                    "phase": "refresh_parse",
                })),
            "qwen: OAuth refresh response is not JSON"
        );
        anyhow::Error::msg("OAuth refresh response is not JSON")
    })?;

    if let Some(error_code) = payload
        .error

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Run `qwen login` again to write fresh tokens to ~/.qwen/oauth_creds.json
  2. If the detail says invalid_grant or expired, the refresh token is dead — only a new login fixes it
  3. For 5xx/timeout details, retry after a short wait (transient endpoint failure)
  4. Check system clock skew and remove stale credential files on CI images
Defensive patterns

Strategy: try-catch

Validate before calling

fn qwen_creds_usable(path: &std::path::Path) -> bool {
    let creds: QwenOauthCredentials = match std::fs::read_to_string(path) {
        Ok(s) => serde_json::from_str(&s).ok()?,
        Err(_) => return false,
    };
    creds.access_token.as_deref().map(|t| !t.trim().is_empty()).unwrap_or(false)
        && creds.refresh_token.is_some()
}

Try / catch

match refresh_qwen_oauth_access_token(&refresh_token, &client_id) {
    Err(e) if e.to_string().contains("HTTP 5") || e.to_string().contains("timed out") => retry_after(backoff),
    Err(e) if e.to_string().contains("invalid_grant") => prompt_relogin("qwen login"), // not retryable
    Err(e) if e.to_string().contains("HTTP 4") => prompt_relogin("qwen login"),
    result => result,
}

Prevention

When it happens

Trigger: POST of grant_type=refresh_token to the Qwen token endpoint returning 400 (revoked or expired refresh_token, invalid client_id), 401, or 5xx; an intercepting proxy returning an HTML error page that is echoed as the detail.

Common situations: Long-lived setups where the Qwen refresh token aged out; several tools sharing ~/.qwen/oauth_creds.json with one of them logging out; CI images shipping a stale credentials file; clock skew making tokens appear expired.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/b0ee880c49c7e779. Report an issue: GitHub.