zeroclaw-labs/zeroclaw · error · anyhow::Error
Timed out waiting for GitHub authorization
Error message
Timed out waiting for GitHub authorization
What it means
The device-flow poll loop ran until its expires_in deadline without the token endpoint returning success or a terminal error - the while condition Instant::now() < expires_at went false while polling was still effectively pending. This is the timeout backstop at the end of device_code_login, not a GitHub-reported error.
Source
Thrown at crates/zeroclaw-providers/src/copilot.rs:606
if let Some(token) = token_response.access_token {
eprintln!("Authentication succeeded.\n");
return Ok(token);
}
match token_response.error.as_deref() {
Some("slow_down") => {
poll_interval += Duration::from_secs(5);
}
Some("authorization_pending") | None => {}
Some("expired_token") => {
anyhow::bail!("GitHub device authorization expired")
}
Some(error) => anyhow::bail!("GitHub auth failed: {error}"),
}
}
anyhow::bail!("Timed out waiting for GitHub authorization")
}
/// Exchange a GitHub access token for a Copilot API key.
async fn exchange_for_api_key(&self, access_token: &str) -> anyhow::Result<ApiKeyInfo> {
let mut request = self.http_client().get(GITHUB_API_KEY_URL);
for (header, value) in &Self::COPILOT_HEADERS {
request = request.header(*header, *value);
}
request = request.header("Authorization", format!("token {access_token}"));
let response = request.send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
let sanitized = super::sanitize_api_error(&body);
if status.as_u16() == 401 || status.as_u16() == 403 {View on GitHub (pinned to 88bb9c8533)
Solutions
- Re-run the login and authorize promptly when the code appears
- Ensure the system clock is correct (NTP) so the expiry window is computed accurately
- Check network stability to github.com - repeated slow polls can consume the window
- If it recurs, verify how long the terminal says the code is valid and complete the browser step immediately
Defensive patterns
Strategy: retry
Try / catch
match copilot_login().await {
Ok(token) => { /* store token */ }
Err(e) if e.to_string().contains("Timed out waiting for GitHub authorization") => {
copilot_login().await // one restart: the previous code is dead anyway
}
Err(e) => Err(e),
} Prevention
- Complete the browser authorization immediately after the code appears
- Keep system clocks NTP-synced so the expiry window is not miscomputed
- Avoid running device login over flaky links; each slow poll eats the window
- Prompt the user again if login has been pending for several minutes
When it happens
Trigger: Polling authorization_pending for the full code lifetime without confirmation: user never completed authorization, poll intervals stretched by network latency, or a system clock jump made expires_at appear to pass early.
Common situations: User took longer than the code lifetime; NTP stepped the clock mid-flow; congested network stretching each poll cycle; user authorized just after the window closed.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- GitHub device authorization expired
- GitHub auth failed: {error}
- Device code expired before authorization was completed
- xAI device-code flow timed out before authorization complete
- ACP elicitation/create timed out after {timeout:?}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e5f3601d42bf8b37.
Report an issue: GitHub.