charmbracelet/crush · error
copilot token request failed: %s - %s
Error message
copilot token request failed: %s - %s
What it means
getCopilotToken exchanges the GitHub OAuth token for a short-lived Copilot API token. A 403 maps to ErrNotAvailable (plan/entitlement missing), but any other non-200 status yields "copilot token request failed: <status> - <body>" with the raw response body. Called by tryGetToken (initial login) and RefreshToken (token renewal).
Source
Thrown at internal/oauth/copilot/oauth.go:176
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusForbidden {
return nil, ErrNotAvailable
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("copilot token request failed: %s - %s", resp.Status, string(body))
}
var result struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
copilotToken := &oauth.Token{
AccessToken: result.Token,
RefreshToken: githubToken,
ExpiresAt: result.ExpiresAt,
}
copilotToken.SetExpiresIn()
return copilotToken, nilView on GitHub (pinned to 7944b8e522)
Solutions
- Read status/body in the error: 401 means re-run the full GitHub device-flow login to get a new token
- Retry with backoff on 5xx — especially in RefreshToken, where a transient failure should not force re-auth
- Verify the Copilot subscription/entitlement is active for the account
- Check proxy/firewall access to the Copilot token endpoint
Example fix
// before
tok, err := RefreshToken(ctx, t)
if err != nil { return err }
// after
tok, err := RefreshToken(ctx, t)
if err != nil {
if strings.Contains(err.Error(), "50") {
time.Sleep(backoff)
tok, err = RefreshToken(ctx, t)
}
if err != nil { return err }
} Defensive patterns
Strategy: retry
Validate before calling
func needsRelogin(err error) bool {
return err != nil && strings.Contains(err.Error(), "copilot token request failed: 401")
} Try / catch
tok, err := RefreshToken(ctx, t)
if err != nil {
if needsRelogin(err) {
return fullDeviceFlowLogin(ctx) // 401: token dead, re-auth
}
if strings.Contains(err.Error(), "50") {
time.Sleep(backoff)
tok, err = RefreshToken(ctx, t)
}
} Prevention
- Treat 401 as re-auth, 5xx as retry, 403 as ErrNotAvailable (no subscription)
- Refresh the Copilot token proactively before expires_at
- Retry refreshes with backoff before invalidating stored credentials
When it happens
Trigger: The GET to the Copilot token endpoint returns e.g. 401 (GitHub token invalid/expired/revoked), 404 (endpoint moved), or 5xx (service outage). 403 is intentionally handled separately as ErrNotAvailable.
Common situations: GitHub session token revoked or expired before refresh; user's Copilot subscription lapsed (may surface as non-403 on some paths); corporate proxy blocking the endpoint; transient GitHub 5xx during RefreshToken causing spurious logout.
Related errors
- device code request failed: %s - %s
- failed to create OAuth handler for mcp %q: %w
- oauth token source: %w
- failed to import copilot: %w
- failed to import copilot: status code %d
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/ba3c426cd9b313cb.
Report an issue: GitHub.