charmbracelet/crush · error
authorization failed: %s
Error message
authorization failed: %s
What it means
tryGetToken maps the token endpoint's "error" field to sentinel errors for known cases (authorization_pending, slow_down); any other error string results in "authorization failed: <code>". This means GitHub rejected the token request with an unexpected OAuth error code such as expired_token, invalid_grant, or unsupported_grant_type.
Source
Thrown at internal/oauth/copilot/oauth.go:145
AccessToken string `json:"access_token"`
Error string `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
switch result.Error {
case "":
if result.AccessToken == "" {
return nil, errPending
}
return getCopilotToken(ctx, result.AccessToken)
case "authorization_pending":
return nil, errPending
case "slow_down":
return nil, errSlowDown
default:
return nil, fmt.Errorf("authorization failed: %s", result.Error)
}
}
func getCopilotToken(ctx context.Context, githubToken string) (*oauth.Token, error) {
req, err := http.NewRequestWithContext(ctx, "GET", copilotTokenURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", githubToken))
for k, v := range Headers() {
req.Header.Set(k, v)
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, errView on GitHub (pinned to 7944b8e522)
Solutions
- Restart the device-flow login with a fresh device code — most causes (expired/denied) require it anyway
- Log the exact error code in the message to distinguish access_denied from expired_token
- If access_denied, ask the user to approve the authorization prompt next time
- Handle expired_token by restarting the flow slightly before expiry instead of polling until failure
Example fix
// before
if err != nil { return err }
// after
if err != nil {
if strings.Contains(err.Error(), "expired_token") {
dc, _ = RequestDeviceCode(ctx) // restart flow
return PollForToken(ctx, dc)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// none: the OAuth error code arrives only during polling
Try / catch
tok, err := PollForToken(ctx, dc)
if err != nil {
if strings.Contains(err.Error(), "authorization failed: expired_token") {
dc, _ = RequestDeviceCode(ctx)
tok, err = PollForToken(ctx, dc)
}
if err != nil { return err }
} Prevention
- Restart the flow on expired_token; do not keep polling a dead code
- Distinguish access_denied (user UX issue) from expired_token (timing) in logs
- Stop polling as soon as the code's expires_in elapses
When it happens
Trigger: PollForToken receives a 200 response whose JSON error field is not one of the three handled values — most commonly expired_token (device code expired between poll cycles) or access_denied (user denied the request).
Common situations: User clicked "Cancel"/denied authorization in the browser; device code expired just as the user confirmed; GitHub changed its error vocabulary; clock skew affecting expiry handling.
Related errors
- device code request failed: %s - %s
- authorization timed out
- ${ErrorDescription}
- copilot token request failed: %s - %s
- create request: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/0d4fedd2488e3f66.
Report an issue: GitHub.