{"record":{"id":"90bc6d379546562a","repo":"charmbracelet/crush","slug":"failed-to-refresh-oauth-token-for-provider-s-w","errorCode":null,"errorMessage":"failed to refresh OAuth token for provider %s: %w","messagePattern":"failed to refresh OAuth token for provider (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":734,"sourceCode":"\n\t// Disk still holds our token (or no newer peer token exists) and we hold\n\t// the lock, so we are the sole exchanger. Perform the exchange.\n\trefreshedToken, refreshErr := s.exchange(ctx, providerID, entryToken.RefreshToken)\n\tif refreshErr != nil {\n\t\t// The exchange may have failed because a peer rotated the refresh\n\t\t// token in a window we did not cover. Re-check disk: adopt a usable\n\t\t// token, or retry once with the peer's newer refresh token.\n\t\tif diskToken := s.newerDiskToken(scope, providerID, entryToken); diskToken != nil {\n\t\t\tif !diskToken.IsExpired() {\n\t\t\t\tslog.Info(\"Adopting token refreshed by another session after exchange failure\", \"provider\", providerID)\n\t\t\t\treturn s.applyToken(providerConfig, diskToken, providerID)\n\t\t\t}\n\t\t\tslog.Info(\"Retrying exchange with refresh token rotated by another session\", \"provider\", providerID)\n\t\t\trefreshedToken, refreshErr = s.exchange(ctx, providerID, diskToken.RefreshToken)\n\t\t}\n\t}\n\tif refreshErr != nil {\n\t\treturn fmt.Errorf(\"failed to refresh OAuth token for provider %s: %w\", providerID, refreshErr)\n\t}\n\n\tslog.Info(\"Successfully refreshed OAuth token\", \"provider\", providerID)\n\tif err := s.applyToken(providerConfig, refreshedToken, providerID); err != nil {\n\t\treturn err\n\t}\n\n\tif err := s.SetConfigFields(scope, map[string]any{\n\t\tfmt.Sprintf(\"providers.%s.api_key\", providerID): refreshedToken.AccessToken,\n\t\tfmt.Sprintf(\"providers.%s.oauth\", providerID):   refreshedToken,\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"failed to persist refreshed token: %w\", err)\n\t}\n\treturn nil\n}\n\n// WaitForTokenChange blocks until SignalAuthComplete is called for the\n// given provider or the context is cancelled. It is used by OnAuthRefresh","sourceCodeStart":716,"sourceCodeEnd":752,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L716-L752","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nrefreshedToken, refreshErr = s.exchange(ctx, providerID, refreshToken)\n// after: detect invalid_grant and force full re-auth\nif refreshErr != nil && strings.Contains(refreshErr.Error(), \"invalid_grant\") {\n    return requireReauth(providerID) // prompt user to log in again\n}","handlingStrategy":"retry","validationCode":"if !hasRefreshableOAuth(providerID) { promptLogin(providerID) }","typeGuard":null,"tryCatchPattern":"if err := refresh(); err != nil {\n    if isAuthError(err) { runReauthFlow(providerID) } else if isTransient(err) { backoffRetry(refresh) }\n}","preventionTips":["Re-authenticate proactively before refresh tokens expire","Avoid running duplicate sessions that rotate the same refresh token","Verify network/proxy access to provider endpoints"],"tags":["oauth","token-refresh","network"],"backgroundTag":"oauth-refresh-token-invalid","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}