Add AbortSignal support to API functions for request cancellation

Add optional signal?: AbortSignal parameter to all API GET functions so they can be
cancelled when components unmount. This prevents state-update warnings and wasted
resources.

Changes:
- frontend/src/api/history.ts: fetchHistory, fetchIpHistory
- frontend/src/api/map.ts: fetchBansByCountry
- frontend/src/api/jails.ts: fetchJails, fetchActiveBans
- frontend/src/api/config.ts: fetchJailConfig, fetchInactiveJails, fetchJailConfigFiles,
  fetchFilterFiles (threads signal through fetchFilters), fetchFilterFile, fetchActionFiles,
  fetchActionFile
- frontend/src/api/blocklist.ts: fetchImportLog, previewBlocklist

Updated all calling hooks to pass the abort signal from their controllers:
- useHistory, useIpHistory
- useMapData
- useActiveBans
- useJails
- useConfigActiveStatus (fetchJails and fetchJailConfigs)
- useJailAdmin (fetchInactiveJails)
- useJailConfigDetail (fetchJailConfig)
- useImportLog (fetchImportLog)
- useBlocklists (previewBlocklist with AbortController)

Updated Docs/Web-Development.md to document the convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-23 08:33:28 +02:00
parent 1c5b2d36d9
commit 5a6cb640d8
16 changed files with 54 additions and 59 deletions

View File

@@ -1,23 +1,3 @@
### TASK-BUG-09 — `linesCount` Input in `ServerHealthSection` Fires Fetch on Every Keystroke
**Where found**
`frontend/src/components/config/ServerHealthSection.tsx` line 410: `onChange={(_e, d) => { setLinesCount(Number(d.value)); }}`. The `linesCount` value is passed directly to `useServerHealth(linesCount, filterValue)`. `useServerHealth` re-creates its `refresh` callback when `linesCount` changes, which rebuilds `fetchData`, which triggers `useEffect([fetchData])`, firing a `GET /api/config/server/log` for every digit typed. Unlike `filterValue` (which is debounced at 500ms), `linesCount` has no debounce.
**Goal**
Introduce a `debouncedLinesCount` state mirroring the existing `filterValue` / `filterRaw` pattern already in the component. Update the `onChange` handler to set a raw state, and apply a debounce (300500ms) before committing to `linesCount` passed to `useServerHealth`.
**Possible traps and issues**
- The debounce ref pattern (`filterDebounceRef`) is already present in the component; the linesCount debounce should reuse the same approach to avoid introducing a second debounce timer ref.
- `Number(d.value)` on an empty field produces `0`. Guard against `0` or negative values before passing to the API, or the backend may reject them.
**Docs changes needed**
None required.
**Why this is needed**
Typing `"500"` in the Lines field currently fires three HTTP requests (`"5"`, `"50"`, `"500"`). Each request fetches potentially hundreds of log lines and serializes them, adding unnecessary backend load.
---
### TASK-ABORT-01 — Missing `signal` Parameter on Multiple API Functions
**Where found**

View File

@@ -76,14 +76,16 @@ import type { Ban } from "../types/ban";
- Use a **central API client** (e.g., a thin wrapper around `fetch` or `axios`) that returns typed data — individual components never call `fetch` directly.
- Validate or assert the response structure at the boundary when dealing with untrusted data; for critical flows, consider a runtime validation library (e.g., `zod`).
- API endpoint paths are **constants** defined in a single file (`api/endpoints.ts`) — never hard-code URLs in components.
- **All API functions that perform a `GET` request must accept an optional `signal?: AbortSignal` parameter and forward it to the HTTP client.** This enables hooks to cancel in-flight requests when components unmount, preventing silent state-update errors and wasted resources. When an API function calls another internal API function, thread the signal through to the underlying call.
```ts
// api/client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";
async function get<T>(path: string): Promise<T> {
async function get<T>(path: string, signal?: AbortSignal): Promise<T> {
const response: Response = await fetch(`${BASE_URL}${path}`, {
credentials: "include",
signal,
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
@@ -99,11 +101,19 @@ export const api = { get, post, put, del } as const;
import type { BanListResponse } from "../types/ban";
import { api } from "./client";
export async function fetchBans(hours: number): Promise<BanListResponse> {
return api.get<BanListResponse>(`/bans?hours=${hours}`);
export async function fetchBans(hours: number, signal?: AbortSignal): Promise<BanListResponse> {
return api.get<BanListResponse>(`/bans?hours=${hours}`, signal);
}
```
```ts
// hooks/useBans.ts
const ctrl = new AbortController();
fetchBans(24, ctrl.signal) // Pass the signal to enable cancellation on unmount
.then(resp => { /* ... */ })
.catch(err => { /* ... */ });
```
---
## 4. Code Organization