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

  1. Re-run the login command to get a fresh device code and complete the browser step promptly within 15 minutes
  2. Make sure the user_code/verification_uri prompt is actually displayed (not swallowed by logging or a daemon context)
  3. Pass a realistic expires_in matching GitHub's response if you drive the poll loop yourself
  4. 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

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


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/6f5f028068c253cb. Report an issue: GitHub.