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>
This commit is contained in:
2026-04-23 08:43:11 +02:00
parent 4e3f2005f9
commit 584588e363
2 changed files with 35 additions and 15 deletions

View File

@@ -237,11 +237,12 @@ export async function unbanAllBans(): Promise<UnbanAllResponse> {
* 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<IpLookupResponse> {
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip));
export async function lookupIp(ip: string, signal?: AbortSignal): Promise<IpLookupResponse> {
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip), signal);
}
// ---------------------------------------------------------------------------

View File

@@ -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<IpLookupResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const lookup = useCallback((ip: string) => {
setLoading(true);
setError(null);
setResult(null);
const lookup = useCallback((ip: string): void => {
(async (): Promise<void> => {
// 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 };
}