From 5446f6c3e1f1b5a5b190cb65c87c7888e338d8ea Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 19 Apr 2026 19:31:03 +0200 Subject: [PATCH] Fix jail banned IP loading race with AbortController --- Docs/Tasks.md | 6 +++- frontend/src/api/jails.ts | 3 +- .../hooks/__tests__/useJailBannedIps.test.ts | 1 + frontend/src/hooks/useJailBannedIps.ts | 28 +++++++++++++++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 9ac78b3..2e62b69 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -16,7 +16,11 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr --- -### TASK-001 — Race condition in `useJailBannedIps`: missing AbortController +### TASK-001 — Race condition in `useJailBannedIps`: missing AbortController (done) + +**Where fixed:** `frontend/src/hooks/useJailBannedIps.ts`, `frontend/src/api/jails.ts` + +**Summary:** Added an `AbortController` ref to cancel stale fetches, passed the signal into `fetchJailBannedIps`, and abort on unmount. **Where found:** `frontend/src/hooks/useJailBannedIps.ts` — the `load` callback is `async` and calls `fetchJailBannedIps` with no AbortSignal. diff --git a/frontend/src/api/jails.ts b/frontend/src/api/jails.ts index c141c32..9ed3b67 100644 --- a/frontend/src/api/jails.ts +++ b/frontend/src/api/jails.ts @@ -266,6 +266,7 @@ export async function fetchJailBannedIps( page = 1, pageSize = 25, search?: string, + signal?: AbortSignal, ): Promise { const params: Record = { page: String(page), @@ -275,5 +276,5 @@ export async function fetchJailBannedIps( params.search = search; } const query = new URLSearchParams(params).toString(); - return get(`${ENDPOINTS.jailBanned(jailName)}?${query}`); + return get(`${ENDPOINTS.jailBanned(jailName)}?${query}`, signal); } diff --git a/frontend/src/hooks/__tests__/useJailBannedIps.test.ts b/frontend/src/hooks/__tests__/useJailBannedIps.test.ts index 4b62eb5..882ed80 100644 --- a/frontend/src/hooks/__tests__/useJailBannedIps.test.ts +++ b/frontend/src/hooks/__tests__/useJailBannedIps.test.ts @@ -18,6 +18,7 @@ describe("useJailBannedIps", () => { expect(result.current.loading).toBe(false); }); expect(result.current.items.length).toBe(1); + expect(fetchMock).toHaveBeenCalledWith("sshd", 1, 25, undefined, expect.any(AbortSignal)); await act(async () => { await result.current.unban("1.2.3.4"); diff --git a/frontend/src/hooks/useJailBannedIps.ts b/frontend/src/hooks/useJailBannedIps.ts index 235fad4..f1ec2a2 100644 --- a/frontend/src/hooks/useJailBannedIps.ts +++ b/frontend/src/hooks/useJailBannedIps.ts @@ -34,8 +34,14 @@ export function useJailBannedIps(jailName: string): UseJailBannedIpsResult { const [error, setError] = useState(null); const [opError, setOpError] = useState(null); const debounceRef = useRef | null>(null); + const abortRef = useRef(null); const load = useCallback(async (): Promise => { + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + const { signal } = controller; + if (!jailName) { setItems([]); setTotal(0); @@ -47,13 +53,27 @@ export function useJailBannedIps(jailName: string): UseJailBannedIpsResult { setError(null); try { - const resp = await fetchJailBannedIps(jailName, page, pageSize, debouncedSearch || undefined); + const resp = await fetchJailBannedIps( + jailName, + page, + pageSize, + debouncedSearch || undefined, + signal, + ); + if (signal.aborted) { + return; + } setItems(resp.items); setTotal(resp.total); } catch (err: unknown) { + if (signal.aborted) { + return; + } handleFetchError(err, setError, "Failed to fetch jailed IPs"); } finally { - setLoading(false); + if (!signal.aborted) { + setLoading(false); + } } }, [jailName, page, pageSize, debouncedSearch]); @@ -76,6 +96,10 @@ export function useJailBannedIps(jailName: string): UseJailBannedIpsResult { useEffect(() => { void load(); + + return (): void => { + abortRef.current?.abort(); + }; }, [load]); const unban = useCallback(async (ip: string): Promise => {