zeroclaw-labs/zeroclaw · error

Device code expired before authorization was completed

Error message

Device code expired before authorization was completed

What it means

poll_device_code_tokens computes a local deadline of now + device.expires_in seconds (Google default 1800, applied via unwrap_or(1800)) before entering its polling loop. If wall-clock time passes that deadline while the token endpoint keeps answering anything other than success, the function bails with this message. It means the user never completed authorization at the verification URL within the code's lifetime.

Source

Thrown at crates/zeroclaw-providers/src/auth/gemini_oauth.rs:260

        user_code,
        verification_uri: verification_url,
        expires_in: device_response.expires_in.unwrap_or(1800),
        interval: device_response.interval.unwrap_or(5),
    })
}

pub async fn poll_device_code_tokens(
    client: &Client,
    client_id: &str,
    client_secret: &str,
    device: &DeviceCodeStart,
) -> Result<TokenSet> {
    let deadline = std::time::Instant::now() + Duration::from_secs(device.expires_in);
    let interval = Duration::from_secs(device.interval.max(5));

    loop {
        if std::time::Instant::now() > deadline {
            anyhow::bail!("Device code expired before authorization was completed");
        }

        tokio::time::sleep(interval).await;

        let form = [
            ("client_id", client_id),
            ("client_secret", client_secret),
            ("device_code", device.device_code.as_str()),
            ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
        ];

        let response = client
            .post(GOOGLE_OAUTH_TOKEN_URL)
            .form(&form)
            .send()
            .await
            .context("Failed to poll device code")?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-run auth login --device-code to get a fresh device code and approve promptly
  2. Use the printed verification_uri_complete link (verification_url?user_code=...) so the code is pre-filled and approval takes seconds
  3. Keep the flow that starts the device code and the human approval close together in scripts
Defensive patterns

Strategy: retry

Validate before calling

// Restart the whole flow when polling runs out: a fresh device code is the only recovery.
// Before polling, make sure a human (or automation) is ready to approve within expires_in.
println!("Visit {} within {}s and enter {}", device.verification_uri, device.expires_in, device.user_code);

Try / catch

match poll_device_code_tokens(client, id, secret, &device).await {
    Ok(tokens) => tokens,
    Err(e) if e.to_string().contains("expired before authorization") => {
        // deadline passed without approval: issue a new device code and poll again
        let device = start_device_code_flow(client, id).await?;
        poll_device_code_tokens(client, id, secret, &device).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling poll_device_code_tokens (from auth login --device-code for gemini) and letting device.expires_in elapse: the user never visits device.verification_uri, never enters device.user_code, or approves only after the 30-minute window closes. Every loop iteration still receives authorization_pending from Google, so the local deadline is what fires.

Common situations: User starts the login, is interrupted, and approves hours later; an automation script starts the device flow long before a human is ready to approve; expires_in was shortened by the provider while the client assumed the 1800s default.

Related errors


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