From 136f41020f1f78003cd78e45749eeb97916b5f4d Mon Sep 17 00:00:00 2001 From: MOHAN Date: Wed, 26 Aug 2026 18:24:08 +0530 Subject: [PATCH] Deduplicate concurrent token refresh to fix login redirect loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard fires several authenticated requests at once (summary, cashflow, merchants, accounts, transactions, plus the app shell's own profile fetch). Once the 60-second access-token cookie expires, all of them 401 together, and apiFetch had each one independently call /api/auth/refresh. Refresh tokens are single-use and rotate on the backend, so only the first of these racing calls succeeded — the rest sent an already-consumed refresh token, got rejected, cleared auth cookies, and hard-redirected to /login. Symptom: land in the app, then get bounced back to login almost immediately, repeatedly. Fix: share one in-flight refresh promise across all callers so a burst of concurrent 401s triggers exactly one refresh call. --- lib/api.ts | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index 75fe0ed..28334df 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -34,25 +34,41 @@ export function clearAuth(): void { localStorage.removeItem(USER_KEY); } +// Shared in-flight refresh promise so concurrent 401s (e.g. several widgets +// loading at once) all await the same refresh call instead of each firing +// their own — refresh tokens are single-use, so racing calls would otherwise +// invalidate each other and force a false session-expired logout. +let refreshInFlight: Promise | null = null; + async function tryRefresh(): Promise { + if (refreshInFlight) return refreshInFlight; + + refreshInFlight = (async () => { + try { + const res = await fetch("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + if (!res.ok) { + clearAuth(); + return false; + } + const payload = (await res.json()) as ApiResponse; + if (payload.error) { + clearAuth(); + return false; + } + return true; + } catch { + clearAuth(); + return false; + } + })(); + try { - const res = await fetch("/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - }); - if (!res.ok) { - clearAuth(); - return false; - } - const payload = (await res.json()) as ApiResponse; - if (payload.error) { - clearAuth(); - return false; - } - return true; - } catch { - clearAuth(); - return false; + return await refreshInFlight; + } finally { + refreshInFlight = null; } }