[Frontend] Shared API client with session-expiry handling, abort, and request dedup #103

Closed
opened 2026-07-14 19:49:30 +00:00 by claude-bot · 0 comments
Contributor

Context

The request() fetch wrapper is copy-pasted across four API modules — src/api/campaigns.js:19, src/api/sessions.js:16, src/api/users.js:8, src/api/planning.js:1 — and inlined again in src/api/auth.js. There is no global 401 handling: auth is checked once at mount (src/hooks/useAuth.jsx:14) and never again. No AbortController exists anywhere in the frontend. The dashboard fires one uncached fetchNextSession per campaign card (src/pages/Dashboard.jsx:44, one SessionCountdown per card at :86).

Current behavior

  • When the 8h session expires mid-use, every API call starts failing but AuthGuard keeps rendering the app — the user is stuck on a dead page instead of being sent to login.
  • Fast navigation lets slow, stale responses resolve after newer ones and overwrite fresh state.
  • Opening the dashboard with N campaigns issues N identical-pattern uncached requests every mount.

Fix / Spec

  1. Create a single src/api/client.js used by all API modules:
    • 401 handling: on any 401 response, clear the auth context and redirect to /login — via a subscribable event or a callback registered by useAuth (avoid circular imports).
    • Abort: accept an AbortSignal option and pass it to fetch; pages/components abort in-flight requests in effect cleanup.
    • Dedup + cache: small in-flight request dedup plus a short-TTL response cache (hand-rolled 10-30s Map keyed by method+URL, GETs only). Do NOT add React Query in this issue.
    • Keep the existing {detail} error-surface contract (errors expose the backend's detail message).
    • Consolidate the duplicated FormData-upload and error-handling variants into the shared client too.
  2. Migrate campaigns.js, sessions.js, users.js, planning.js, and auth.js onto the shared client; delete the copy-pasted wrappers.
  3. Wire abort signals into the pages with the heaviest effect-driven fetching (Dashboard, CampaignDetail, WikiArticle at minimum).

Acceptance criteria

  • Test: a mocked expired session (API returns 401) clears auth and redirects to login.
  • Test with delayed mocks: rapid route flips do not apply stale data from the earlier route.
  • Dashboard issues one request per unique campaign per TTL window (dedup verified with mock call counts).
  • All src/api/* modules import the shared client; grep "async function request" in src/api/ returns nothing.
  • Error messages still surface the backend detail string.

References

  • src/api/campaigns.js:19, src/api/sessions.js:16, src/api/users.js:8, src/api/planning.js:1, src/api/auth.js
  • src/hooks/useAuth.jsx:14
  • src/pages/Dashboard.jsx:44, :86

Filed from the July 2026 full-project review.

## Context The `request()` fetch wrapper is copy-pasted across four API modules — `src/api/campaigns.js:19`, `src/api/sessions.js:16`, `src/api/users.js:8`, `src/api/planning.js:1` — and inlined again in `src/api/auth.js`. There is no global 401 handling: auth is checked once at mount (`src/hooks/useAuth.jsx:14`) and never again. No `AbortController` exists anywhere in the frontend. The dashboard fires one uncached `fetchNextSession` per campaign card (`src/pages/Dashboard.jsx:44`, one `SessionCountdown` per card at `:86`). ## Current behavior - When the 8h session expires mid-use, every API call starts failing but `AuthGuard` keeps rendering the app — the user is stuck on a dead page instead of being sent to login. - Fast navigation lets slow, stale responses resolve after newer ones and overwrite fresh state. - Opening the dashboard with N campaigns issues N identical-pattern uncached requests every mount. ## Fix / Spec 1. Create a single `src/api/client.js` used by all API modules: - **401 handling**: on any 401 response, clear the auth context and redirect to `/login` — via a subscribable event or a callback registered by `useAuth` (avoid circular imports). - **Abort**: accept an `AbortSignal` option and pass it to `fetch`; pages/components abort in-flight requests in effect cleanup. - **Dedup + cache**: small in-flight request dedup plus a short-TTL response cache (hand-rolled 10-30s `Map` keyed by method+URL, GETs only). **Do NOT add React Query in this issue.** - Keep the existing `{detail}` error-surface contract (errors expose the backend's `detail` message). - Consolidate the duplicated FormData-upload and error-handling variants into the shared client too. 2. Migrate `campaigns.js`, `sessions.js`, `users.js`, `planning.js`, and `auth.js` onto the shared client; delete the copy-pasted wrappers. 3. Wire abort signals into the pages with the heaviest effect-driven fetching (Dashboard, CampaignDetail, WikiArticle at minimum). ## Acceptance criteria - [ ] Test: a mocked expired session (API returns 401) clears auth and redirects to login. - [ ] Test with delayed mocks: rapid route flips do not apply stale data from the earlier route. - [ ] Dashboard issues one request per unique campaign per TTL window (dedup verified with mock call counts). - [ ] All `src/api/*` modules import the shared client; `grep "async function request"` in `src/api/` returns nothing. - [ ] Error messages still surface the backend `detail` string. ## References - `src/api/campaigns.js:19`, `src/api/sessions.js:16`, `src/api/users.js:8`, `src/api/planning.js:1`, `src/api/auth.js` - `src/hooks/useAuth.jsx:14` - `src/pages/Dashboard.jsx:44`, `:86` _Filed from the July 2026 full-project review._
rbrooks referenced this issue from a commit 2026-07-17 15:23:55 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#103
No description provided.