charmbracelet/crush · error

${ErrorDescription}

Error message

${ErrorDescription}

What it means

During the Hyper OAuth device-code flow, PollForToken receives an error field in the token response other than 'authorization_pending'. The server-supplied ErrorDescription is returned verbatim as a Go error, so the message content comes from the OAuth server.

Source

Thrown at internal/oauth/hyper/device.go:112

	for {
		select {
		case <-ctx.Done():
			return "", ctx.Err()
		case <-ticker.C:
			result, err := pollOnce(ctx, deviceCode)
			if err != nil {
				return "", err
			}
			if result.RefreshToken != "" {
				event.Alias(result.UserID)
				return result.RefreshToken, nil
			}
			switch result.Error {
			case "authorization_pending":
				continue
			default:
				return "", errors.New(result.ErrorDescription)
			}
		}
	}
}

func pollOnce(ctx context.Context, deviceCode string) (TokenResponse, error) {
	var result TokenResponse
	url := fmt.Sprintf("%s/device/auth/%s", hyper.BaseURL(), deviceCode)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return result, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "crush")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Restart the device-code flow (call BeginDeviceAuth / request a fresh device code) since the current code is no longer pending.
  2. Show the returned ErrorDescription to the user so they know what the authorization server rejected.
  3. Handle well-known codes (slow_down, expired_token, access_denied) explicitly before treating the error as fatal.
  4. Retry with a shorter polling interval if the error was slow_down.

Example fix

// before
res, err := flow.PollForToken(ctx, code)
if err != nil { return err }
// after
res, err := flow.PollForToken(ctx, code)
if err != nil {
    if strings.Contains(err.Error(), "expired_token") {
        return restartDeviceFlow(ctx)
    }
    return fmt.Errorf("device authorization failed: %w", err)
}
Defensive patterns

Strategy: retry

Try / catch

token, err := flow.PollForToken(ctx, code)
if err != nil {
    switch {
    case strings.Contains(err.Error(), "slow_down"):
        // back off and retry
    case strings.Contains(err.Error(), "expired_token"):
        return restartDeviceFlow(ctx)
    default:
        return err
    }
}

Prevention

When it happens

Trigger: Polling the token endpoint while the device grant is in a terminal error state: the user denied the request, the device code expired (expired_token), or the server returned any unrecognized error code.

Common situations: User clicks 'cancel' on the GitHub-style authorization page and then the poller gets 'access_denied'; long waits past ExpiresIn yield 'expired_token'; network proxies inject unexpected error codes.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/3f754298e671c43c. Report an issue: GitHub.