{"record":{"id":"6f5f028068c253cb","repo":"headroomlabs-ai/headroom","slug":"github-device-authorization-expired","errorCode":null,"errorMessage":"GitHub device authorization expired.","messagePattern":"GitHub device authorization expired\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/copilot_auth.py","lineNumber":595,"sourceCode":"        with urllib_request.urlopen(request, timeout=timeout) as response:\n            payload = json.loads(response.read().decode(\"utf-8\", errors=\"replace\"))\n        if not isinstance(payload, dict):\n            raise RuntimeError(\"GitHub device authorization returned an invalid response.\")\n\n        access_token = payload.get(\"access_token\")\n        if isinstance(access_token, str) and access_token.strip():\n            return access_token.strip()\n\n        error = str(payload.get(\"error\") or \"\").strip()\n        if error == \"authorization_pending\":\n            time.sleep(poll_interval)\n            continue\n        if error == \"slow_down\":\n            poll_interval += 5\n            time.sleep(poll_interval)\n            continue\n        if error == \"expired_token\":\n            raise RuntimeError(\"GitHub device authorization expired.\")\n        if error:\n            description = str(payload.get(\"error_description\") or error).strip()\n            raise RuntimeError(f\"GitHub device authorization failed: {description}\")\n\n        time.sleep(poll_interval)\n\n    raise RuntimeError(\"GitHub device authorization expired.\")\n\n\ndef _extract_oauth_token(entry: dict[str, Any]) -> str | None:\n    if _entry_expired(entry):\n        return None\n\n    for key in _OAUTH_TOKEN_KEYS:\n        value = entry.get(key)\n        if isinstance(value, str) and value.strip():\n            return value.strip()\n","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/copilot_auth.py#L577-L613","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    token = poll_copilot_device_authorization(device_code, ...)\nexcept RuntimeError as e:\n    if \"expired\" in str(e):\n        # device_code is dead — must start a completely new device flow\n        auth = start_copilot_device_authorization(...)\n        token = poll_copilot_device_authorization(auth[\"device_code\"], ...)\n    else:\n        raise","preventionTips":["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"],"tags":["auth","oauth","github","device-flow","timeout","expiration"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}