Concurrent 401s each call /auth/refresh and can log the user out #49
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happens
During UI verification of the shopping list feature (#47), the headless browser was logged out twice ("Deine Sitzung ist abgelaufen") about an hour after login. Both times
POST /auth/refreshitself returned 401, even though the refresh token is valid for 30 days (REFRESH_TOKEN_TTL_MSinrecipe-backend/src/handlers/AuthHandler.ts). The access token lives 1 h (recipe-backend/src/utils/encryptionUtils.ts:33), which matches the timing.This matters most for the shopping list: the main use case is opening the app in a shop after it sat in a pocket for a while, which is exactly when the access token has expired.
Likely mechanism (not fully proven)
apiRequest(src/api/apiClient.ts:99) callsrefresh()separately for every request that gets a 401. Nothing shares one refresh between concurrent requests.AuthHandler.ts:155). A refresh that arrives with an already-rotated cookie finds no row and gets 401 (AuthHandler.ts:134).apiRequesttreats a failed refresh as an expired session:clearSession()andSESSION_EXPIRED_EVENT(apiClient.ts:104-105), so one losing refresh logs the user out even if another one succeeded.Repro of the backend half, with one cookie jar and two refreshes at an offset:
Close-together refreshes both succeed (both read the row before either deletes it); a later one with the same cookie fails. In the browser log the failed refreshes were within ~50 ms of each other, which doesn't fully fit this window, so the exact trigger is still open. A successful refresh isn't logged as an error, so the log can't show whether one succeeded first.
Suggested fix
apiClient. Keep one in-flight refresh promise, let every concurrent 401 await that same promise and then retry. Only clear the session when that shared refresh fails.How to check
Log in, wait for (or force) access-token expiry, then open
/shopping-list/<id>(parallel requests on mount). With the fix, the page loads after one refresh and the user stays logged in.🤖 Generated with Claude Code