# Token Refresh Fix Design > **Date:** 2026-02-04 > **Status:** Approved > **Issue:** After idle period, app shows "Failed to load trees" with 401 cascade --- ## Problem When the access token expires (15 minutes), the Axios response interceptor attempts to refresh it by calling `POST /api/v1/auth/refresh`. This call fails because of a request/response mismatch: - **Frontend** sends the refresh token in the `Authorization: Bearer ` header - **Backend** expects `refresh_token` as a bare `str` query/body parameter FastAPI cannot extract a bare string parameter from the Authorization header, so the refresh call returns 422 (missing required parameter). The interceptor catches this as a generic failure, but the error propagates to the calling component rather than redirecting to login — leaving the user stuck on a broken page. Additionally, when multiple API calls (trees, folders, categories) all return 401 simultaneously, each independently triggers its own refresh attempt, creating a race condition. ## Root Cause `backend/app/api/endpoints/auth.py:156-158`: ```python async def refresh_token( refresh_token: str, # FastAPI expects query/body param db: ... ) ``` `frontend/src/api/client.ts:37-41`: ```typescript await axios.post(url, null, { headers: { Authorization: `Bearer ${refreshToken}` } // Sent as header }) ``` ## Solution ### 1. Backend: Add `get_refresh_token_payload` dependency Create a proper FastAPI dependency in `deps.py` that extracts and validates a refresh token from the Authorization header, mirroring the existing `get_current_user` pattern: ```python async def get_refresh_token_payload( token: Annotated[str, Depends(oauth2_scheme)] ) -> dict: payload = decode_token(token) if payload is None or payload.get("type") != "refresh": raise HTTPException(status_code=401, detail="Invalid refresh token") return payload ``` ### 2. Backend: Refactor refresh endpoint Change `refresh_token()` in `auth.py` to use the new dependency instead of a bare string parameter. ### 3. Frontend: Add refresh queue (single-flight pattern) When multiple requests hit 401 simultaneously, only the first triggers the actual refresh call. Others queue up and retry with the new token once the refresh completes. ### 4. Frontend: Sync auth store after refresh Add a `setTokens` action to `authStore.ts`. Call it from the interceptor after a successful refresh so the Zustand store stays consistent with localStorage. ## Files Changed | File | Change | |------|--------| | `backend/app/api/deps.py` | Add `get_refresh_token_payload` dependency | | `backend/app/api/endpoints/auth.py` | Use new dependency in refresh endpoint | | `frontend/src/api/client.ts` | Refresh queue + auth store sync | | `frontend/src/store/authStore.ts` | Add `setTokens` action | ## Testing - Set `ACCESS_TOKEN_EXPIRE_MINUTES=1` temporarily to reproduce quickly - Verify silent refresh: login, wait >1 min, interact — no errors visible - Verify concurrent requests: page load after expiry triggers one refresh, all requests succeed - Verify refresh token expiry: after 7 days, clean redirect to login - Run existing backend tests (`pytest`) to confirm no regressions - Run frontend build (`npm run build`) to confirm no compile errors