{"record":{"id":"b0ee880c49c7e779","repo":"zeroclaw-labs/zeroclaw","slug":"oauth-refresh-failed-http-status-detail","errorCode":null,"errorMessage":"OAuth refresh failed (HTTP {status}): {detail}","messagePattern":"OAuth refresh failed \\(HTTP (.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/lib.rs","lineNumber":335,"sourceCode":"            );\n            anyhow::Error::msg(format!(\"OAuth refresh request failed: {error}\"))\n        })?;\n\n    let status = response.status();\n    let body = response\n        .text()\n        .unwrap_or_else(|_| \"<failed to read Qwen OAuth response body>\".to_string());\n\n    let parsed = serde_json::from_str::<QwenOauthTokenResponse>(&body).ok();\n\n    if !status.is_success() {\n        let detail = parsed\n            .as_ref()\n            .and_then(|payload| payload.error_description.as_deref())\n            .or_else(|| parsed.as_ref().and_then(|payload| payload.error.as_deref()))\n            .filter(|msg| !msg.trim().is_empty())\n            .unwrap_or(body.as_str());\n        anyhow::bail!(\"OAuth refresh failed (HTTP {status}): {detail}\");\n    }\n\n    let payload = parsed.ok_or_else(|| {\n        ::zeroclaw_log::record!(\n            ERROR,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                .with_attrs(::serde_json::json!({\n                    \"oauth_provider\": \"qwen\",\n                    \"phase\": \"refresh_parse\",\n                })),\n            \"qwen: OAuth refresh response is not JSON\"\n        );\n        anyhow::Error::msg(\"OAuth refresh response is not JSON\")\n    })?;\n\n    if let Some(error_code) = payload\n        .error","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/lib.rs#L317-L353","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"fn qwen_creds_usable(path: &std::path::Path) -> bool {\n    let creds: QwenOauthCredentials = match std::fs::read_to_string(path) {\n        Ok(s) => serde_json::from_str(&s).ok()?,\n        Err(_) => return false,\n    };\n    creds.access_token.as_deref().map(|t| !t.trim().is_empty()).unwrap_or(false)\n        && creds.refresh_token.is_some()\n}","typeGuard":null,"tryCatchPattern":"match refresh_qwen_oauth_access_token(&refresh_token, &client_id) {\n    Err(e) if e.to_string().contains(\"HTTP 5\") || e.to_string().contains(\"timed out\") => retry_after(backoff),\n    Err(e) if e.to_string().contains(\"invalid_grant\") => prompt_relogin(\"qwen login\"), // not retryable\n    Err(e) if e.to_string().contains(\"HTTP 4\") => prompt_relogin(\"qwen login\"),\n    result => result,\n}","preventionTips":["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"],"tags":["qwen","oauth","refresh-token","http-status","authentication"],"backgroundTag":"oauth-refresh-token-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}