feat(auth): token refresh retry on 401 responses#111
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds public tryRefreshToken() method that wraps the existing private refreshAccessToken() with token persistence and state updates, then returns the new access token string for use by the upcoming UnauthorizedInterceptor. Also configures jest-preset-angular for the web-auth-data-access library so Angular testing utilities work correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Spotify Web Playback SDK calls getOAuthToken multiple times (on expiry, reconnect, wake from sleep). Previously the callback captured a static token snapshot, so after a refresh the SDK kept using the stale token, causing authentication_error and NO_ACTIVE_DEVICE on play. Now reads the latest token from localStorage each time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the LOCALSTORAGE_KEYS constant from auth.store.ts (private) to local-storage.service.ts (exported) so both AuthStore and PlaybackService can reference keys without hardcoded strings or circular dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per spotify/web-api#1325, passing device_id avoids 404 "No active device found" errors when Spotify loses track of the active device. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlayerApiService now stores the deviceId from transferUserPlayback() and automatically appends it as a query param on play() calls. Per spotify/web-api#1325, this avoids 404 "No active device found". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99f23a0 to
187a198
Compare
| player.addListener('authentication_error', ({ message }) => { | ||
| console.error(message); | ||
| console.error('[Angular Spotify] Authentication error, reconnecting...', message); | ||
| player.disconnect(); | ||
| player.connect(); | ||
| }); |
There was a problem hiding this comment.
🔴 Unbounded reconnect loop on authentication_error when token is still invalid
The authentication_error handler calls player.disconnect() then player.connect() with no retry limit, delay, or guard. When connect() runs, it invokes the getOAuthToken callback at playback.service.ts:53, which reads the token from localStorage. If the token in localStorage is still stale (e.g., the interceptor's 401 refresh hasn't completed yet, or the refresh itself failed), the SDK receives the same invalid token, fails to authenticate, and fires authentication_error again — creating a tight infinite loop of disconnect→connect→authenticate→fail→disconnect→connect… This will spam Spotify's servers with rapid WebSocket connection attempts, flood the console, and potentially freeze the browser tab.
Prompt for agents
The authentication_error listener in playback.service.ts (lines 62-66) calls player.disconnect() then player.connect() unconditionally, with no retry limit or backoff. If the token stored in localStorage is still invalid when connect() triggers getOAuthToken, the SDK will fail again and fire authentication_error again, creating an infinite loop.
To fix this, add a reconnect attempt counter and a maximum retry limit (e.g., 3 attempts). Additionally, consider adding an exponential backoff delay (e.g., using setTimeout) between reconnect attempts. Reset the counter on a successful ready event. If the max retries are exceeded, log a final error and stop retrying.
Relevant code:
- playback.service.ts lines 62-66: the authentication_error handler
- playback.service.ts lines 52-53: the getOAuthToken callback that reads from localStorage
- playback.service.ts lines 97-103: the ready listener that fires on successful reconnect (good place to reset the counter)
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
tryRefreshToken()public method toAuthStorethat refreshes the Spotify access token, persists new tokens to localStorage and store state, and returns the new access tokenUnauthorizedInterceptorto silently refresh expired tokens on 401 responses (up to 2 attempts) and replay the original failed request with the fresh tokenFlow
Test plan
AuthStore.tryRefreshToken()(success, localStorage persistence, state update, missing token error, API error propagation)UnauthorizedInterceptor(passthrough, non-401 errors, skip URLs, refresh+replay, 2 failures then modal, second attempt success, concurrent 401 coalescing, concurrent failure propagation)access_tokenfrom localStorage while app is running → verify silent refreshaccess_tokenandrefresh_token→ verify modal appears🤖 Generated with Claude Code