{"record":{"id":"9f4623e300445ad0","repo":"headroomlabs-ai/headroom","slug":"copilot-token-exchange-returned-an-empty-token","errorCode":null,"errorMessage":"Copilot token exchange returned an empty token.","messagePattern":"Copilot token exchange returned an empty token\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"headroom/copilot_auth.py","lineNumber":1186,"sourceCode":"            if not _should_exchange_oauth_token():\n                direct_token = CopilotAPIToken(\n                    token=oauth_token,\n                    expires_at=time.time() + 3600,\n                    api_url=_configured_api_url(),\n                )\n                self._cached = direct_token\n                return direct_token\n\n            exchanged = await self._exchange_token(oauth_token)\n            self._cached = exchanged\n            return exchanged\n\n    async def _exchange_token(self, oauth_token: str) -> CopilotAPIToken:\n        headers = _copilot_token_exchange_headers(oauth_token)\n        payload = await asyncio.to_thread(self._exchange_token_sync, headers)\n        token = str(payload.get(\"token\") or \"\").strip()\n        if not token:\n            raise RuntimeError(\"Copilot token exchange returned an empty token.\")\n\n        expires_at = _parse_expiry(payload.get(\"expires_at\")) or (time.time() + 1800)\n        api_url = await asyncio.to_thread(\n            _api_url_from_exchange_payload,\n            payload,\n            oauth_token=oauth_token,\n        )\n        refresh_in = payload.get(\"refresh_in\")\n        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","sourceCodeStart":1168,"sourceCodeEnd":1204,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/copilot_auth.py#L1168-L1204","documentation":"Raised in CopilotTokenProvider._exchange_token after a successful HTTP exchange with GitHub's Copilot token endpoint: the response payload parsed, but payload['token'] is missing, empty, or whitespace-only after strip. This means the OAuth credential was accepted enough to get a 200 response yet no usable API token came back — most often the Copilot entitlement is absent from the GitHub account.","triggerScenarios":"Exchange GET to _token_exchange_url() returns 200 with JSON lacking a 'token' field — e.g. the account has no Copilot subscription (free/paid) attached, the SKU is not provisioned, or an unexpected payload shape from a changed endpoint.","commonSituations":"Personal access token or OAuth token from an account without Copilot access; organization Copilot seat not yet assigned; attempting immediately after seat purchase before provisioning; endpoint contract drift returning an error object with HTTP 200.","solutions":["Confirm the GitHub account actually has a Copilot entitlement (individual subscription or an org seat) at github.com/settings/copilot","If a seat was just assigned, wait briefly and retry — provisioning can lag","Re-login to refresh the OAuth token in case it belongs to a different account than the one with Copilot","Inspect the exchange response (log payload keys, never the token) to distinguish 'no entitlement' from shape drift"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    api_token = await provider.get_token()\nexcept RuntimeError as e:\n    if \"empty token\" in str(e):\n        raise SystemExit(\n            \"Copilot exchange returned no token — verify the account has a Copilot \"\n            \"subscription/seat at github.com/settings/copilot\"\n        ) from e\n    raise","preventionTips":["Verify the Copilot entitlement on the account before scripting token exchange","After assigning an org seat, allow provisioning time before first use","Log exchange payload keys (never values) to distinguish entitlement gaps from endpoint changes"],"tags":["auth","copilot","token-exchange","entitlement","subscription"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}