{"record":{"id":"83649564969407cf","repo":"headroomlabs-ai/headroom","slug":"copilot-token-exchange-failed-with-http-exc-code","errorCode":null,"errorMessage":"Copilot token exchange failed with HTTP {exc.code}: {body}","messagePattern":"Copilot token exchange failed with HTTP (.+?): (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/copilot_auth.py","lineNumber":1213,"sourceCode":"        sku = payload.get(\"sku\")\n        return CopilotAPIToken(\n            token=token,\n            expires_at=expires_at,\n            api_url=api_url,\n            refresh_in=int(refresh_in) if isinstance(refresh_in, int | float) else None,\n            sku=str(sku) if isinstance(sku, str) and sku.strip() else None,\n        )\n\n    @staticmethod\n    def _exchange_token_sync(headers: dict[str, str]) -> dict[str, Any]:\n        request = urllib_request.Request(_token_exchange_url(), headers=headers, method=\"GET\")\n        try:\n            with urllib_request.urlopen(request, timeout=10.0) as response:\n                payload = json.loads(response.read().decode(\"utf-8\"))\n                return payload if isinstance(payload, dict) else {}\n        except urllib_error.HTTPError as exc:\n            body = exc.read().decode(\"utf-8\", errors=\"replace\")\n            raise RuntimeError(\n                f\"Copilot token exchange failed with HTTP {exc.code}: {body}\"\n            ) from exc\n\n\n_provider: CopilotTokenProvider | None = None\n\n\ndef get_copilot_token_provider() -> CopilotTokenProvider:\n    \"\"\"Return the shared Copilot token provider.\"\"\"\n\n    global _provider\n    if _provider is None:\n        _provider = CopilotTokenProvider()\n    return _provider\n\n\ndef _is_copilot_api_token(token: str) -> bool:\n    \"\"\"Return True when the token looks like a short-lived Copilot API token.","sourceCodeStart":1195,"sourceCodeEnd":1231,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/copilot_auth.py#L1195-L1231","documentation":"Raised by CopilotTokenProvider._exchange_token_sync when the GET to the Copilot token-exchange endpoint returns an HTTPError (4xx/5xx). The RuntimeError embeds both exc.code and the decoded response body, preserving GitHub's error JSON for diagnosis. Common codes: 401 bad/expired OAuth token, 403 no Copilot entitlement or forbidden client, 404 wrong exchange URL (e.g. GHE misconfig).","triggerScenarios":"Exchanging an expired or revoked OAuth refresh token (401); account without Copilot access (403); _token_exchange_url() pointing at a host/path that does not exist for the configured domain (404); transient 5xx from GitHub.","commonSituations":"Cached OAuth token older than its lifetime so exchange 401s; GitHub Enterprise hosts lacking the Copilot endpoints; rotating/revoking the OAuth grant in GitHub settings; short GitHub outages surfacing as 502/503.","solutions":["Read the embedded HTTP code: 401 → re-run the device login to mint a fresh OAuth token; 403 → verify Copilot entitlement/seat; 404 → check the configured domain/_token_exchange_url for GHE","For 5xx, retry after a short backoff — the exchange is a plain GET and safe to repeat","If the token was revoked in GitHub settings, clear the headroom auth cache and re-login","Wrap callers of get_copilot_token_provider() token fetches so this RuntimeError surfaces as a re-auth prompt rather than a crash"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"import time\n\nfor attempt in range(3):\n    try:\n        api_token = await provider.get_token()\n        break\n    except RuntimeError as e:\n        msg = str(e)\n        if \"HTTP 5\" in msg and attempt < 2:\n            time.sleep(2 ** attempt)  # transient GitHub error: back off and retry\n            continue\n        if \"HTTP 401\" in msg:\n            trigger_device_login()  # credential expired: re-auth, do not retry\n            continue\n        raise","preventionTips":["Classify by the embedded HTTP code: 401 → re-login, 403 → entitlement, 5xx → retry with backoff","Cache CopilotAPIToken until near expires_at so exchanges are rare","Never log the response body of a failed exchange in shared logs — it can contain account identifiers"],"tags":["auth","copilot","http","token-exchange","network"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}