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

xAI device-code polling failed ({status}): {}

Error message

xAI device-code polling failed ({status}): {}

What it means

`poll_device_code_tokens` received a structured OAuth error it does not special-case — everything except authorization_pending, slow_down, access_denied/authorization_denied, and expired_token. The endpoint's `error_description` (or bare `error`) is embedded with the HTTP status. Codes like `invalid_grant` (bad or replayed device_code) or `invalid_client` land here.

Source

Thrown at crates/zeroclaw-providers/src/auth/xai_oauth.rs:311

        if response.status().is_success() {
            return parse_token_response(response).await;
        }

        let status = response.status();
        let text = response.text().await.unwrap_or_default();
        if let Ok(err) = serde_json::from_str::<OAuthErrorResponse>(&text) {
            match err.error.as_str() {
                "authorization_pending" => continue,
                "slow_down" => {
                    interval_secs = interval_secs.saturating_add(5);
                    continue;
                }
                "access_denied" | "authorization_denied" => {
                    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)
            );

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the embedded error code: `invalid_grant` usually means the device_code is stale — start a fresh flow
  2. Use one consistent set of discovery results and client constants for the whole flow
  3. For unrecognized codes, check xAI status/changelog — the grant may have changed
Defensive patterns

Strategy: try-catch

Type guard

fn is_device_poll_error(e: &anyhow::Error) -> bool {
    e.to_string().contains("xAI device-code polling failed")
}

Try / catch

match poll_device_code_tokens(&client, &ep, &device).await {
    Ok(t) => t,
    Err(e) if is_device_poll_error(&e) && e.to_string().contains("invalid_grant") => {
        let device = start_device_code_flow(&client, &dev_ep).await?; // stale device_code
        poll_device_code_tokens(&client, &ep, &device).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Polling with a device_code from a different client or an older session; xAI rejecting the urn:ietf:params:oauth:grant-type:device_code grant or the client_id; unexpected provider-side error codes.

Common situations: Reusing a stale `DeviceCodeStart` across process restarts; provider changes to the device grant; fixtures with hardcoded device codes.

Related errors


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