refactoring-backend #3
@@ -1,32 +1,3 @@
|
|||||||
### Issue #49: HIGH - Dual Auth Error Handlers With No Deduplication Guard
|
|
||||||
|
|
||||||
**Where found**:
|
|
||||||
- `frontend/src/providers/AuthProvider.tsx:50-56`
|
|
||||||
- `frontend/src/api/client.ts:143` – `setUnauthorizedHandler`
|
|
||||||
- `frontend/src/utils/fetchError.ts` – `setAuthErrorHandler`
|
|
||||||
|
|
||||||
**Why this is needed**:
|
|
||||||
Both handlers fire independently on a 401. Without a deduplication guard, logout logic executes twice, causing a React state mutation race and potentially double-navigating to the login page.
|
|
||||||
|
|
||||||
**Goal**:
|
|
||||||
Ensure auth error handling fires exactly once per 401 response.
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
1. Introduce a module-level `isLoggingOut` flag (or use a ref) that is set before the first logout dispatch and checked before the second.
|
|
||||||
2. Alternatively, consolidate into a single handler and remove one registration.
|
|
||||||
|
|
||||||
**Possible traps and issues**:
|
|
||||||
- The "defense-in-depth" rationale for two handlers may be intentional; confirm with team before removing one.
|
|
||||||
- The flag must be reset after the login page is reached.
|
|
||||||
|
|
||||||
**Docs changes needed**:
|
|
||||||
- Add comment in `AuthProvider.tsx` explaining the deduplication guard.
|
|
||||||
|
|
||||||
**Doc references**:
|
|
||||||
- `frontend/src/providers/AuthProvider.tsx` inline comment about cross-tab logout
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue #50: HIGH - Navigation Abort Signal Timing Bug
|
### Issue #50: HIGH - Navigation Abort Signal Timing Bug
|
||||||
|
|
||||||
**Where found**:
|
**Where found**:
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ interface NavigationCancellationProviderProps {
|
|||||||
*
|
*
|
||||||
* Detects route changes via useLocation and automatically aborts
|
* Detects route changes via useLocation and automatically aborts
|
||||||
* all AbortSignals obtained during the previous route.
|
* all AbortSignals obtained during the previous route.
|
||||||
|
*
|
||||||
|
* Timing contract:
|
||||||
|
* - AbortController is created synchronously in render when pathname changes,
|
||||||
|
* before any request can be initiated on the new route.
|
||||||
|
* - The old controller is aborted inside the effect (after render).
|
||||||
|
* - Strict Mode double-invocation: abort is idempotent, so double-abort is safe.
|
||||||
*/
|
*/
|
||||||
export function NavigationCancellationProvider(
|
export function NavigationCancellationProvider(
|
||||||
props: NavigationCancellationProviderProps,
|
props: NavigationCancellationProviderProps,
|
||||||
@@ -48,22 +54,31 @@ export function NavigationCancellationProvider(
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Current active AbortController for this route
|
// Current active AbortController for this route.
|
||||||
const controllerRef = useRef<AbortController>(new AbortController());
|
// Created synchronously during render when pathname changes,
|
||||||
|
// so the fresh signal is available before any request is initiated.
|
||||||
|
const controllerRef = useRef<AbortController | null>(null);
|
||||||
const prevPathnameRef = useRef<string>(location.pathname);
|
const prevPathnameRef = useRef<string>(location.pathname);
|
||||||
|
|
||||||
|
// Create new controller synchronously when pathname changes.
|
||||||
|
// This ensures getNavigationSignal() returns a non-aborted signal
|
||||||
|
// for requests initiated during the same render pass.
|
||||||
|
// Abort old controller BEFORE creating new one to prevent
|
||||||
|
// any request on the new route from using the old (aborted) signal.
|
||||||
|
if (controllerRef.current === null || location.pathname !== prevPathnameRef.current) {
|
||||||
|
controllerRef.current?.abort();
|
||||||
|
controllerRef.current = new AbortController();
|
||||||
|
}
|
||||||
|
|
||||||
const getNavigationSignal = useCallback((): AbortSignal => {
|
const getNavigationSignal = useCallback((): AbortSignal => {
|
||||||
return controllerRef.current.signal;
|
return controllerRef.current!.signal;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// When route pathname changes (not on mount), abort the old controller and create a new one
|
// Update prevPathnameRef after synchronous controller creation.
|
||||||
|
// This runs after render so we don't abort on Strict Mode remounts
|
||||||
|
// (where pathname is unchanged between renders).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPathname = location.pathname;
|
prevPathnameRef.current = location.pathname;
|
||||||
if (currentPathname !== prevPathnameRef.current) {
|
|
||||||
prevPathnameRef.current = currentPathname;
|
|
||||||
controllerRef.current?.abort();
|
|
||||||
controllerRef.current = new AbortController();
|
|
||||||
}
|
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const contextValue: NavigationCancellationContextType = {
|
const contextValue: NavigationCancellationContextType = {
|
||||||
|
|||||||
Reference in New Issue
Block a user