Wei-Shaw/sub2api · error
TOKEN_REFRESH_FAILED
TOKEN_REFRESH_FAILED
Error message
Session expired. Please log in again.
What it means
Thrown by the axios interceptor when the refresh token flow fails and the session has NOT changed (the localStorage refresh_token/auth_user still match the pre-refresh snapshot). Before rejecting, the interceptor clears all auth keys (auth_token, refresh_token, auth_user, token_expires_at), sets sessionStorage 'auth_expired=1', and hard-redirects to /login unless already there. The rejection carries status 401 and code TOKEN_REFRESH_FAILED, meaning the refresh token was absent, invalid, revoked, or the refresh endpoint errored.
Source
Thrown at frontend/src/api/client.ts:216
code: 'AUTH_SESSION_CHANGED',
message: 'Authentication session changed while refreshing.'
})
}
// Clear tokens and redirect to login
localStorage.removeItem('auth_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('auth_user')
localStorage.removeItem('token_expires_at')
sessionStorage.setItem('auth_expired', '1')
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login'
}
return Promise.reject({
status: 401,
code: 'TOKEN_REFRESH_FAILED',
message: 'Session expired. Please log in again.'
})
}
}
// No refresh token or is auth endpoint - clear auth and redirect
const hasToken = !!localStorage.getItem('auth_token')
const headers = error.config?.headers as Record<string, unknown> | undefined
const authHeader = headers?.Authorization ?? headers?.authorization
const sentAuth =
typeof authHeader === 'string'
? authHeader.trim() !== ''
: Array.isArray(authHeader)
? authHeader.length > 0
: !!authHeader
localStorage.removeItem('auth_token')
localStorage.removeItem('refresh_token')View on GitHub (pinned to 073e92d171)
Solutions
- Have the user log in again — the redirect to /login with sessionStorage 'auth_expired=1' is the designed recovery; show a 'session expired' message there.
- If this happens immediately after login, verify the login response actually persists refresh_token to localStorage and that the refresh endpoint/URL is correct.
- If it happens randomly, check server-side revocation logs and refresh-token rotation: a rotated token reused by a second tab will fail refresh.
- Confirm the system clock on client and server is correct (skew can cause premature 401s).
Example fix
// before
// app boots, calls API, silently bounces to /login with no explanation
sessionStorage.removeItem('auth_expired')
// after
// on the login page, surface the reason
if (sessionStorage.getItem('auth_expired') === '1') {
sessionStorage.removeItem('auth_expired')
toast.info('Session expired. Please log in again.')
} Defensive patterns
Strategy: try-catch
Type guard
function isTokenRefreshFailed(e: unknown): e is { status: number; code: 'TOKEN_REFRESH_FAILED'; message: string } {
return typeof e === 'object' && e !== null && (e as any).code === 'TOKEN_REFRESH_FAILED'
} Try / catch
try {
await apiClient.get('/me')
} catch (e) {
if (isTokenRefreshFailed(e)) {
// Interceptor already cleared storage and set auth_expired=1 + redirect.
// Surface a friendly message; do not retry with the wiped credentials.
toast.info('Session expired. Please log in again.')
return
}
throw e
} Prevention
- Keep the app's tab active or implement a proactive refresh timer before token_expires_at elapses.
- On login, verify refresh_token is actually persisted to localStorage before navigating away.
- Handle refresh-token rotation: never reuse an already-consumed refresh token from a second tab.
- Check sessionStorage 'auth_expired' on the login page to explain why the user was redirected.
When it happens
Trigger: A 401 on a protected endpoint with a refresh token that the backend rejects (expired/revoked/rotated elsewhere); a 401 with no refresh token in localStorage while an Authorization header was sent; a 401 on an auth endpoint itself. In all cases the stored session still matches, so the client wipes it and redirects to /login.
Common situations: Long-lived SPA left open past refresh-token lifetime; refresh token revoked server-side (password change, admin revoke); clock skew making token_expires_at stale; backend restart losing sessions; another deployment changing auth endpoints.
Related errors
- AUTH_SESSION_CHANGED
- Passkeys are not supported by this browser
- Passkey sign-in was cancelled
- Passkey creation was cancelled
- HTTP error! status: ${response.status}
AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15).
Data as JSON: /api/errors/1dd89ea8b87c9520.
Report an issue: GitHub.