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
.errorView on GitHub (pinned to 88bb9c8533)
Solutions
- Run `qwen login` again to write fresh tokens to ~/.qwen/oauth_creds.json
- If the detail says invalid_grant or expired, the refresh token is dead — only a new login fixes it
- For 5xx/timeout details, retry after a short wait (transient endpoint failure)
- 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
- Run `qwen login` during environment provisioning, not lazily at first chat
- Refresh tokens proactively on a schedule instead of waiting for expiry
- Do not retry invalid_grant — only a new login resolves it
- Keep ~/.qwen/oauth_creds.json private to one tool to avoid logout races
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
- token refresh failed ({status}): {body}
- MiniMax OAuth refresh failed (HTTP {status}): {detail}
- OAuth refresh failed: {detail}
- MiniMax OAuth refresh failed: {detail}
- LinkedIn token refresh failed ({}): {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b0ee880c49c7e779.
Report an issue: GitHub.