From 584588e363485f3cdd5168fee0807bcb8ae4c662 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 23 Apr 2026 08:43:11 +0200 Subject: [PATCH] fix: add AbortController and unmount guard to useIpLookup hook - Add AbortController to cancel pending IP lookups when component unmounts - Prevent state updates on unmounted components by checking abort signal before setState calls - Add useEffect cleanup that aborts pending requests on unmount - Update lookupIp API function to accept optional AbortSignal parameter - Converts callback-based fetch to async/await pattern for better control flow - Aligns with other API functions that already support abort signals (fetchJails, fetchJailBannedIps) Fixes TASK-ABORT-04 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/api/jails.ts | 5 ++-- frontend/src/hooks/useIpLookup.ts | 45 ++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/frontend/src/api/jails.ts b/frontend/src/api/jails.ts index 16d5b78..de49a92 100644 --- a/frontend/src/api/jails.ts +++ b/frontend/src/api/jails.ts @@ -237,11 +237,12 @@ export async function unbanAllBans(): Promise { * Look up ban status and geo-location for an IP address. * * @param ip - IP address to look up. + * @param signal - Optional abort signal for request cancellation. * @returns An {@link IpLookupResponse} with ban history and geo info. * @throws {ApiError} On non-2xx responses (400 for invalid IP). */ -export async function lookupIp(ip: string): Promise { - return get(ENDPOINTS.geoLookup(ip)); +export async function lookupIp(ip: string, signal?: AbortSignal): Promise { + return get(ENDPOINTS.geoLookup(ip), signal); } // --------------------------------------------------------------------------- diff --git a/frontend/src/hooks/useIpLookup.ts b/frontend/src/hooks/useIpLookup.ts index ffbc74b..a58696e 100644 --- a/frontend/src/hooks/useIpLookup.ts +++ b/frontend/src/hooks/useIpLookup.ts @@ -2,7 +2,7 @@ * React hook for looking up a single IP address. */ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { handleFetchError } from "../utils/fetchError"; import { lookupIp } from "../api/jails"; import type { IpLookupResponse } from "../types/jail"; @@ -22,22 +22,34 @@ export function useIpLookup(): UseIpLookupResult { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const abortRef = useRef(null); - const lookup = useCallback((ip: string) => { - setLoading(true); - setError(null); - setResult(null); + const lookup = useCallback((ip: string): void => { + (async (): Promise => { + // Abort any previous lookup + abortRef.current?.abort(); + const ctrl = new AbortController(); + abortRef.current = ctrl; - lookupIp(ip) - .then((res) => { + setLoading(true); + setError(null); + setResult(null); + + try { + const res = await lookupIp(ip, ctrl.signal); + if (ctrl.signal.aborted) return; setResult(res); - }) - .catch((err: unknown) => { + } catch (err: unknown) { + if (ctrl.signal.aborted) return; handleFetchError(err, setError, "Failed to lookup IP"); - }) - .finally(() => { - setLoading(false); - }); + } finally { + if (!ctrl.signal.aborted) { + setLoading(false); + } + } + })().catch(() => { + // Silently ignore abort errors + }); }, []); const clear = useCallback(() => { @@ -45,5 +57,12 @@ export function useIpLookup(): UseIpLookupResult { setError(null); }, []); + // Cleanup: abort any pending lookup on unmount + useEffect(() => { + return (): void => { + abortRef.current?.abort(); + }; + }, []); + return { result, loading, error, lookup, clear }; }