charmbracelet/crush · error
failed to refresh OAuth token for provider %s: %w
Error message
failed to refresh OAuth token for provider %s: %w
What it means
After acquiring the refresh lock, the store exchanges/refreshes the OAuth token with the provider. If the exchange call fails (including after one retry with a refresh token that a peer session rotated), the error is wrapped as 'failed to refresh OAuth token for provider %s'. It signals the provider rejected the refresh attempt.
Source
Thrown at internal/config/store.go:734
// Disk still holds our token (or no newer peer token exists) and we hold
// the lock, so we are the sole exchanger. Perform the exchange.
refreshedToken, refreshErr := s.exchange(ctx, providerID, entryToken.RefreshToken)
if refreshErr != nil {
// The exchange may have failed because a peer rotated the refresh
// token in a window we did not cover. Re-check disk: adopt a usable
// token, or retry once with the peer's newer refresh token.
if diskToken := s.newerDiskToken(scope, providerID, entryToken); diskToken != nil {
if !diskToken.IsExpired() {
slog.Info("Adopting token refreshed by another session after exchange failure", "provider", providerID)
return s.applyToken(providerConfig, diskToken, providerID)
}
slog.Info("Retrying exchange with refresh token rotated by another session", "provider", providerID)
refreshedToken, refreshErr = s.exchange(ctx, providerID, diskToken.RefreshToken)
}
}
if refreshErr != nil {
return fmt.Errorf("failed to refresh OAuth token for provider %s: %w", providerID, refreshErr)
}
slog.Info("Successfully refreshed OAuth token", "provider", providerID)
if err := s.applyToken(providerConfig, refreshedToken, providerID); err != nil {
return err
}
if err := s.SetConfigFields(scope, map[string]any{
fmt.Sprintf("providers.%s.api_key", providerID): refreshedToken.AccessToken,
fmt.Sprintf("providers.%s.oauth", providerID): refreshedToken,
}); err != nil {
return fmt.Errorf("failed to persist refreshed token: %w", err)
}
return nil
}
// WaitForTokenChange blocks until SignalAuthComplete is called for the
// given provider or the context is cancelled. It is used by OnAuthRefreshView on GitHub (pinned to 7944b8e522)
Solutions
- Re-authenticate with the provider (run the login/auth flow) to obtain a fresh refresh token.
- Check network connectivity and proxy settings to the provider endpoint.
- Ensure only one session/tooling rotates the refresh token; stop duplicate sessions sharing the config.
- Inspect the wrapped provider error for 4xx vs 5xx to decide between re-auth and retry.
Example fix
// before
refreshedToken, refreshErr = s.exchange(ctx, providerID, refreshToken)
// after: detect invalid_grant and force full re-auth
if refreshErr != nil && strings.Contains(refreshErr.Error(), "invalid_grant") {
return requireReauth(providerID) // prompt user to log in again
} Defensive patterns
Strategy: retry
Validate before calling
if !hasRefreshableOAuth(providerID) { promptLogin(providerID) } Try / catch
if err := refresh(); err != nil {
if isAuthError(err) { runReauthFlow(providerID) } else if isTransient(err) { backoffRetry(refresh) }
} Prevention
- Re-authenticate proactively before refresh tokens expire
- Avoid running duplicate sessions that rotate the same refresh token
- Verify network/proxy access to provider endpoints
When it happens
Trigger: s.exchange(ctx, providerID, refreshToken) returns an error: invalid/expired refresh token, provider API outage, network failure, or the token was rotated by another session and the retry with the disk token also failed.
Common situations: Refresh token revoked (password change, admin revocation, grant rotation by a parallel CLI); Copilot/Hyper token expiry after long offline periods; corporate proxy blocking provider endpoints.
Related errors
- failed to refresh OAuth token: %w
- failed to get MCP pending auth: %w
- provider %s not found
- provider %s does not have an OAuth token
- copilot token request failed: %s - %s
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/90bc6d379546562a.
Report an issue: GitHub.