Skip to content

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 persistSessionsetStoredSessionsetItemAsync hang on the oversized SecureStore write?

Resources: - main/app/lib/api/getApi.ts — axios instance and request interceptor - main/app/lib/api/auth/session.tsgetValidIdToken - 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.tsuseMemoriesFeed 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

  1. Let the stored session expire (or clear sessionCache so a SecureStore read + refresh is required).
  2. Make the device unable to reach Cognito (airplane mode, blocked network, or slow Wi-Fi).
  3. Open the memory feed screen.
  4. Observe feed_query_requested in logs, then silence — no feed_query_succeeded or feed_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