fix: dedupe parallel admin 401 redirects to /admin/login
Visiting /admin/dashboard while logged out caused a navigation storm:
the dashboard fires ~7 /api/admin/* queries on mount, each returns 401,
each axios interceptor call did `window.location.href = '/admin/login'`.
The path-based guard `currentPath.includes('/admin/login')` reads
`location.pathname` *synchronously* — but `location.href = …` is async,
so all 7 parallel handlers saw the still-old pathname and each fired a
fresh navigation. The browser logged 6+ ERR_ABORTED entries and the user
saw a flicker storm. Same shape would bite any admin page that fans out
queries on mount.
Add a module-level `adminLoginRedirectPending` flag set the moment we
kick off the first redirect; subsequent 401s in the same tick see it
and skip. Single navigation, clean transition to login.
Smoke spec 10-admin-redirect-loop locks the regression in by sampling
the URL across 5 ticks — if any tick lands somewhere other than
/admin/login, the spec fails.
This commit is contained in:
@@ -11,6 +11,12 @@ import { getApiBaseUrl } from '../utils/url';
|
||||
// Maintenance mode callback
|
||||
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
||||
|
||||
// Set true the moment we kick off a hard redirect to /admin/login so
|
||||
// subsequent 401s in the same tick don't queue more navigations on top
|
||||
// (each `window.location.href = …` aborts the previous, producing a
|
||||
// flicker storm — see the response interceptor below).
|
||||
let adminLoginRedirectPending = false;
|
||||
|
||||
export const setMaintenanceModeCallback = (callback: (enabled: boolean) => void) => {
|
||||
maintenanceModeCallback = callback;
|
||||
};
|
||||
@@ -131,10 +137,17 @@ api.interceptors.response.use(
|
||||
// Check if it's an admin route (but not public endpoints)
|
||||
const isAdminRoute = error.config?.url?.includes('/admin') && !error.config?.url?.includes('/public/');
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
|
||||
if (isAdminRoute) {
|
||||
// Only redirect if we're not already on the admin login page
|
||||
if (!currentPath.includes('/admin/login')) {
|
||||
// Only redirect if we're not already on the admin login page.
|
||||
// `window.location.href = …` is async — `pathname` doesn't change
|
||||
// synchronously — so a fan-out of 401s (the dashboard fires 7 admin
|
||||
// queries in parallel) would each see the old pathname and each
|
||||
// call `location.href`, producing a navigation storm where every
|
||||
// request is aborted by the next. Guard with a module-level flag
|
||||
// so only the first 401 triggers the redirect.
|
||||
if (!currentPath.includes('/admin/login') && !adminLoginRedirectPending) {
|
||||
adminLoginRedirectPending = true;
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user