Memory Feed Load Stuck — Token Refresh Has No Timeout In Axios Interceptor
Metadata
- Date:
2026-04-18 - Status:
fixed - Severity:
high - Related issue/ticket:
N/A - Owner:
N/A
About
Overview: - Memory feed fires feed_query_requested then produces no further logs — no success, no error. - The load spinner never resolves and the UI hangs indefinitely. - SecureStore warnings about values > 2048 bytes are emitted alongside the hang.
Technical Questions: - Does the hang originate in the axios request interceptor before the HTTP request is sent? - Is refreshSession (Cognito callback wrapped in a bare Promise) the source of the indefinite wait? - Is the axios timeout: 10_000 on the client irrelevant here because it only applies after the request is dispatched? - Does persistSession → setStoredSession → setItemAsync hang on the oversized SecureStore write?
Resources: - main/app/lib/api/getApi.ts — axios instance and request interceptor - main/app/lib/api/auth/session.ts — getValidIdToken - main/app/lib/api/auth/refreshSession.ts — Cognito callback Promise with no timeout - main/app/lib/api/auth/auth-provider.tsx — has withTimeout guard (existing pattern) - main/app/lib/api/memory/useMemoryApi.ts — useMemoriesFeed queryFn
Steps to cause failure
flowchart LR
FeedQuery["useMemoriesFeed queryFn fires"] --> ListMemories["listMemories → getApi().post()"]
ListMemories --> Interceptor["axios request interceptor awaits getValidIdToken()"]
Interceptor --> CacheExpired["session cache expired or null"]
CacheExpired --> Refresh["refreshSession() — Cognito callback Promise, no timeout"]
Refresh --> CognitoSlow["Cognito slow / network issue — callback never fires"]
CognitoSlow --> HangForever["Promise never resolves/rejects"]
HangForever --> NoLogs["feed_query_succeeded and feed_query_failed never emit"] System
flowchart TD
useMemoriesFeed --> listMemories
listMemories --> getApi
getApi --> RequestInterceptor["request interceptor: await getValidIdToken()"]
RequestInterceptor --> sessionCache{"cache valid?"}
sessionCache -->|yes| Token["return idToken"]
sessionCache -->|no| getStoredSession
getStoredSession --> storedExpired{"stored expired?"}
storedExpired -->|no| Token
storedExpired -->|yes| refreshSession["refreshSession(username, refreshToken)"]
refreshSession --> CognitoCallback["CognitoUser.refreshSession — callback, no timeout"]
CognitoCallback -->|success| persistSession
CognitoCallback -->|network hang| HangForever["Promise never settles"] The auth-provider.tsx bootstrap wraps all getValidIdToken() calls with withTimeout(..., 5000). The axios request interceptor in getApi.ts does not — it awaits getValidIdToken() with no guard.
Reproduction Details
- Let the stored session expire (or clear
sessionCacheso a SecureStore read + refresh is required). - Make the device unable to reach Cognito (airplane mode, blocked network, or slow Wi-Fi).
- Open the memory feed screen.
- Observe
feed_query_requestedin logs, then silence — nofeed_query_succeededorfeed_query_failed.
Reproduction test: unit test in main/app/__tests__/getApi.test.ts that stubs getValidIdToken to return a never-resolving Promise and asserts the interceptor rejects within 5 s.
Notes for PR
Root cause: - refreshSession wraps CognitoUser.refreshSession (a callback API) in new Promise(...) with no timeout. - The axios request interceptor await getValidIdToken() inherits this indefinite wait. - The axios timeout: 10_000 only starts a countdown after the request is dispatched — it has no effect while the interceptor is still running. - auth-provider.tsx already demonstrates the correct pattern: withTimeout(getValidIdToken(), 5000).
Fix summary: - Added withAuthTimeout helper in getApi.ts (mirrors withTimeout in auth-provider.tsx) using Promise.race with a 5 s setTimeout. - Wrapped getValidIdToken() call in the request interceptor with withAuthTimeout(...). - If Cognito is unreachable the interceptor now rejects after 5 s with "Auth token fetch timed out after 5000ms", surfacing as feed_query_failed in the hook.
Verification summary: - Reproduction test (__tests__/getApi.test.ts) failed before fix (Jest wall-clock timeout); passes after fix in 54 ms. - Token-attach and null-token paths continue to pass. - No regressions in existing test suites.
Audit Log
| ID | Action | Note | Context |
|---|---|---|---|
| 1 | Create audit log | Initialized systematic debugging record for memory feed hang | issue created |
| 2 | Trace feed dependencies | feed_query_requested logs but no success/error — hang is before HTTP dispatch, in the request interceptor | scope narrowing |
| 3 | Identify root cause | refreshSession uses bare callback Promise with no timeout; interceptor awaits it unbounded; axios timeout irrelevant at this stage | root cause identified |
| 4 | Confirm existing pattern | auth-provider.tsx uses withTimeout(getValidIdToken(), 5000) — same guard needed in getApi.ts interceptor | pattern match |
| 5 | Write reproduction test | __tests__/getApi.test.ts — stubs getValidIdToken as never-resolving; confirmed failure pre-fix (Jest timeout) | reproducible failure |
| 6 | Apply fix | Added withAuthTimeout in getApi.ts, wrapped interceptor call; mirrors auth-provider pattern | source fix |
| 7 | Verify with tests | All 3 interceptor tests pass; no regressions | validation |
Verification
- [x] Reproduced failure before fix
- [x] Reproduction test fails before fix
- [x] Root cause identified with evidence
- [x] Fix applied at source (no workaround-only patch)
- [x] Reproduction test passes after fix
- [x] Reproduction path now passes
- [x] Regression test added/updated (
__tests__/getApi.test.ts) - [x] Verified no duplicate solved-bug log exists for same root cause