{"record":{"id":"178460c16fbae12a","repo":"Wei-Shaw/sub2api","slug":"auth-session-changed","errorCode":"AUTH_SESSION_CHANGED","errorMessage":"Authentication session changed while refreshing.","messagePattern":"Authentication session changed while refreshing\\.","errorType":"error_code","errorClass":null,"httpStatus":401,"severity":"error","filePath":"frontend/src/api/client.ts","lineNumber":198,"sourceCode":"                ? authHeader.slice('Bearer '.length)\n                : null\n            const tokens = await refreshAuthTokens({ failedAccessToken })\n\n            // Retry the original request with the refreshed token\n            if (originalRequest.headers) {\n              originalRequest.headers.Authorization = `Bearer ${tokens.access_token}`\n            }\n            return apiClient(originalRequest)\n          } catch {\n            // A stale request must never destroy a session that was logged out or replaced while\n            // its refresh was in flight (for example, when another tab signs in as another user).\n            const sessionChanged =\n              localStorage.getItem('refresh_token') !== refreshToken ||\n              localStorage.getItem('auth_user') !== refreshSessionUser\n            if (sessionChanged) {\n              return Promise.reject({\n                status: 401,\n                code: 'AUTH_SESSION_CHANGED',\n                message: 'Authentication session changed while refreshing.'\n              })\n            }\n\n            // Clear tokens and redirect to login\n            localStorage.removeItem('auth_token')\n            localStorage.removeItem('refresh_token')\n            localStorage.removeItem('auth_user')\n            localStorage.removeItem('token_expires_at')\n            sessionStorage.setItem('auth_expired', '1')\n\n            if (!window.location.pathname.includes('/login')) {\n              window.location.href = '/login'\n            }\n\n            return Promise.reject({\n              status: 401,\n              code: 'TOKEN_REFRESH_FAILED',","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/frontend/src/api/client.ts#L180-L216","documentation":"Thrown by the axios response interceptor in frontend/src/api/client.ts when a token refresh attempt fails AND the locally stored session (refresh_token or auth_user in localStorage) no longer matches the values captured before the refresh started. It signals that another tab or flow replaced or logged out the session while this request's refresh was in flight. The rejection carries status 401 and code AUTH_SESSION_CHANGED so callers can distinguish it from a plain expired session. Importantly, this path deliberately does NOT clear the stored tokens, because the newer session in localStorage is authoritative.","triggerScenarios":"A request gets a 401, enters the refresh flow, and the refresh call fails; meanwhile localStorage['refresh_token'] or localStorage['auth_user'] changed (e.g. another tab signed in as a different user, or a logout happened between capturing refreshToken/refreshSessionUser and the failed refresh). Any stale in-flight request from the old session then rejects with this error.","commonSituations":"Multi-tab apps where one tab re-authenticates while another has stale in-flight requests; logout in one tab during a refresh in another; embedded iframes or PWA service-worker requests that outlive a session swap; race between a sign-in redirect and queued 401 retries.","solutions":["If a login/redirect flow is racing in-flight API calls, cancel or await pending requests before replacing localStorage auth keys.","In the app's axios error handler, treat code==='AUTH_SESSION_CHANGED' as non-fatal: do not force redirect, just invalidate the affected request (refetch with the new token or silently drop it).","Ensure only one tab performs refresh (e.g. use a BroadcastChannel/web-lock around the refresh call) so stale refreshes stop competing with new sessions.","Verify nothing in your code writes localStorage 'auth_user' or 'refresh_token' from stale state after a successful login elsewhere."],"exampleFix":"// before\napiClient.get('/me').catch((e) => {\n  if (e.status === 401) window.location.href = '/login' // also fires for AUTH_SESSION_CHANGED\n})\n\n// after\napiClient.get('/me').catch((e) => {\n  if (e.code === 'AUTH_SESSION_CHANGED') return retryWithFreshToken('/me') // session is valid, just this request is stale\n  if (e.status === 401) window.location.href = '/login'\n})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isAuthSessionChanged(e: unknown): e is { status: number; code: 'AUTH_SESSION_CHANGED'; message: string } {\n  return typeof e === 'object' && e !== null && (e as any).code === 'AUTH_SESSION_CHANGED'\n}","tryCatchPattern":"try {\n  await apiClient.get('/me')\n} catch (e) {\n  if (isAuthSessionChanged(e)) {\n    // Session in localStorage is newer than this request; retry with fresh state\n    // instead of redirecting — the tokens were intentionally NOT cleared.\n    return retryAfterReauth()\n  }\n  throw e\n}","preventionTips":["Serialize refresh attempts across tabs (BroadcastChannel or Web Locks) so only one refresh runs at a time.","Before overwriting localStorage auth keys on login/logout, abort in-flight requests holding old tokens.","Never auto-redirect to /login on code AUTH_SESSION_CHANGED — that path is reserved for TOKEN_REFRESH_FAILED.","Read the current token from storage per request, not from a module-level cache."],"tags":["auth","frontend","axios","token-refresh","race-condition"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}