headroomlabs-ai/headroom · error · RuntimeError
GitHub device authorization expired.
Error message
GitHub device authorization expired.
What it means
Raised when GitHub's token endpoint explicitly returns error=expired_token during the device-code poll: the user did not complete browser authorization within the flow's lifetime (default 900s / 15 minutes). It is a terminal RuntimeError — the device_code is dead and polling cannot continue, so the caller must start a new device authorization.
Source
Thrown at headroom/copilot_auth.py:595
with urllib_request.urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8", errors="replace"))
if not isinstance(payload, dict):
raise RuntimeError("GitHub device authorization returned an invalid response.")
access_token = payload.get("access_token")
if isinstance(access_token, str) and access_token.strip():
return access_token.strip()
error = str(payload.get("error") or "").strip()
if error == "authorization_pending":
time.sleep(poll_interval)
continue
if error == "slow_down":
poll_interval += 5
time.sleep(poll_interval)
continue
if error == "expired_token":
raise RuntimeError("GitHub device authorization expired.")
if error:
description = str(payload.get("error_description") or error).strip()
raise RuntimeError(f"GitHub device authorization failed: {description}")
time.sleep(poll_interval)
raise RuntimeError("GitHub device authorization expired.")
def _extract_oauth_token(entry: dict[str, Any]) -> str | None:
if _entry_expired(entry):
return None
for key in _OAUTH_TOKEN_KEYS:
value = entry.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
View on GitHub (pinned to 322425c43b)
Solutions
- Re-run the login command to get a fresh device code and complete the browser step promptly within 15 minutes
- Make sure the user_code/verification_uri prompt is actually displayed (not swallowed by logging or a daemon context)
- Pass a realistic expires_in matching GitHub's response if you drive the poll loop yourself
- Automate recovery: catch this RuntimeError and restart the device flow once
Defensive patterns
Strategy: try-catch
Try / catch
try:
token = poll_copilot_device_authorization(device_code, ...)
except RuntimeError as e:
if "expired" in str(e):
# device_code is dead — must start a completely new device flow
auth = start_copilot_device_authorization(...)
token = poll_copilot_device_authorization(auth["device_code"], ...)
else:
raise Prevention
- Complete the browser authorization promptly after the prompt appears (15-minute window)
- Surface user_code/verification_uri visibly (TTY or logged) — hidden prompts cause most expirations
- Automate one automatic re-flow on expiration instead of failing the whole session
When it happens
Trigger: poll_copilot_device_authorization() runs for the full window (or GitHub reports expired_token early) because the user never opened verification_uri, never entered the user_code, or denied/abandoned the consent page. The sibling raise at the loop's end (error 109) covers the local expires_in deadline expiring without GitHub saying expired_token.
Common situations: Headless/SSH sessions where the verification URL is not clickable; user steps away from the terminal; long-polling wrapped in code that suppresses the prompt showing user_code; clock skew or a very short expires_in passed by the caller.
Related errors
- GitHub device authorization returned an invalid response.
- GitHub device authorization failed: {description}
- Copilot OAuth token must not be empty.
- No GitHub Copilot OAuth token is available.
- bedrock_sigv4_failed
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/6f5f028068c253cb.
Report an issue: GitHub.