headroomlabs-ai/headroom · error · RuntimeError
Copilot token exchange failed with HTTP {exc.code}: {body}
Error message
Copilot token exchange failed with HTTP {exc.code}: {body} What it means
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).
Source
Thrown at headroom/copilot_auth.py:1213
sku = payload.get("sku")
return CopilotAPIToken(
token=token,
expires_at=expires_at,
api_url=api_url,
refresh_in=int(refresh_in) if isinstance(refresh_in, int | float) else None,
sku=str(sku) if isinstance(sku, str) and sku.strip() else None,
)
@staticmethod
def _exchange_token_sync(headers: dict[str, str]) -> dict[str, Any]:
request = urllib_request.Request(_token_exchange_url(), headers=headers, method="GET")
try:
with urllib_request.urlopen(request, timeout=10.0) as response:
payload = json.loads(response.read().decode("utf-8"))
return payload if isinstance(payload, dict) else {}
except urllib_error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(
f"Copilot token exchange failed with HTTP {exc.code}: {body}"
) from exc
_provider: CopilotTokenProvider | None = None
def get_copilot_token_provider() -> CopilotTokenProvider:
"""Return the shared Copilot token provider."""
global _provider
if _provider is None:
_provider = CopilotTokenProvider()
return _provider
def _is_copilot_api_token(token: str) -> bool:
"""Return True when the token looks like a short-lived Copilot API token.View on GitHub (pinned to 322425c43b)
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
Defensive patterns
Strategy: retry
Try / catch
import time
for attempt in range(3):
try:
api_token = await provider.get_token()
break
except RuntimeError as e:
msg = str(e)
if "HTTP 5" in msg and attempt < 2:
time.sleep(2 ** attempt) # transient GitHub error: back off and retry
continue
if "HTTP 401" in msg:
trigger_device_login() # credential expired: re-auth, do not retry
continue
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Copilot token exchange returned an empty token.
- upstream request failed: {0}
- Copilot OAuth token must not be empty.
- GitHub device authorization returned an invalid response.
- No GitHub Copilot OAuth token is available.
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/83649564969407cf.
Report an issue: GitHub.