zeroclaw-labs/zeroclaw · error · anyhow::Error
xAI OAuth token request failed ({status}): {}
Error message
xAI OAuth token request failed ({status}): {} What it means
xAI's `parse_token_response` — shared by `exchange_code_for_tokens`, `refresh_access_token`, and device polling — got a non-2xx whose body parsed as a standard OAuth error document; the provider's `error_description` (falling back to `error`) is embedded with the status. This is the informative variant: the provider told you exactly what failed.
Source
Thrown at crates/zeroclaw-providers/src/auth/xai_oauth.rs:326
anyhow::bail!("xAI device-code authorization was denied")
}
"expired_token" => anyhow::bail!("xAI device-code expired"),
_ => anyhow::bail!(
"xAI device-code polling failed ({status}): {}",
err.error_description.unwrap_or(err.error)
),
}
}
anyhow::bail!("xAI device-code polling failed ({status}): {text}");
}
}
async fn parse_token_response(response: reqwest::Response) -> Result<TokenSet> {
let status = response.status();
let body = response.text().await.unwrap_or_default();
if !status.is_success() {
if let Ok(err) = serde_json::from_str::<OAuthErrorResponse>(&body) {
anyhow::bail!(
"xAI OAuth token request failed ({status}): {}",
err.error_description.unwrap_or(err.error)
);
}
anyhow::bail!("xAI OAuth token request failed ({status}): {body}");
}
let parsed: TokenResponse =
serde_json::from_str(&body).context("Failed to parse xAI OAuth token response")?;
let expires_at = parsed
.expires_in
.map(|secs| Utc::now() + chrono::Duration::seconds(secs))
.or_else(|| derive_expires_at_from_jwt(&parsed.access_token));
Ok(TokenSet {
access_token: parsed.access_token,
refresh_token: parsed.refresh_token,
id_token: parsed.id_token,View on GitHub (pinned to 88bb9c8533)
Solutions
- Map the embedded code: `invalid_grant` → restart login for a fresh code or refresh token; check the PKCE pair when exchanging
- Persist the PkceState before opening the browser and restore it verbatim with `restore_pkce_state`
- Ensure `redirect_uri` is exactly `http://127.0.0.1:56121/callback` (XAI_OAUTH_REDIRECT_URI)
- Write the new refresh token to the store immediately after every refresh (rotation invalidates the old one)
Example fix
// before: new random PKCE at exchange time — challenge unknown to xAI let pkce = generate_pkce_state(); let tokens = exchange_code_for_tokens(&client, &token_ep, &code, &pkce).await?; // invalid_grant // after: persist the original verifier+state and restore them exactly let pkce = restore_pkce_state(saved_code_verifier, saved_state); let tokens = exchange_code_for_tokens(&client, &token_ep, &code, &pkce).await?;
Defensive patterns
Strategy: try-catch
Validate before calling
// before opening the browser, make sure the PKCE pair is restorable let pkce = generate_pkce_state(); save_pkce_to_state(&pkce).await?; // verifier + state survive a crash let url = build_authorize_url(&disc.authorization_endpoint, &pkce);
Type guard
fn is_xai_invalid_grant(e: &anyhow::Error) -> bool {
let s = e.to_string();
s.contains("xAI OAuth token request failed") && s.contains("invalid_grant")
} Try / catch
let tokens = match refresh_access_token(&client, &refresh).await {
Ok(t) => t,
Err(e) if is_xai_invalid_grant(&e) => run_interactive_login().await?,
Err(e) => return Err(e),
}; Prevention
- Persist the PkceState (verifier + state) before opening the browser; restore it verbatim with restore_pkce_state
- Exchange the authorization code once, immediately
- Rotate the stored refresh token right after every refresh
When it happens
Trigger: `invalid_grant` when the authorization code expired or was replayed, or when the PKCE pair mismatches (xAI revalidates `code_challenge` and `code_challenge_method` at the token endpoint); a refresh token that was revoked or rotated; `invalid_client`.
Common situations: Exchanging a code twice after a crash; a verifier from a different PkceState than the challenge sent in the authorize URL; `restore_pkce_state` rebuilt with the wrong saved verifier; old refresh token superseded by a login elsewhere.
Related errors
- xAI OAuth token request failed ({status}): {body}
- OpenAI OAuth token request failed ({status}): {body}
- xAI OAuth discovery failed ({status}): {body}
- xAI device-code start failed ({status}): {body}
- xAI device-code polling failed ({status}): {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c6307f535b3b24f0.
Report an issue: GitHub.