Standardise frontend hook fetch error handling and mark Task 12 done

This commit is contained in:
2026-03-22 10:17:15 +01:00
parent bf2abda595
commit 136f21ecbe
17 changed files with 62 additions and 45 deletions

View File

@@ -263,7 +263,7 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue.
---
### Task 12 — Standardise error handling in frontend hooks
### Task 12 — Standardise error handling in frontend hooks (✅ completed)
**Priority**: Low
**Refactoring ref**: Refactoring.md §9

View File

@@ -7,6 +7,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchBanTrend } from "../api/dashboard";
import { handleFetchError } from "../utils/fetchError";
import type { BanTrendBucket, BanOriginFilter, TimeRange } from "../types/ban";
// ---------------------------------------------------------------------------
@@ -65,7 +66,7 @@ export function useBanTrend(
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to fetch trend data");
handleFetchError(err, setError, "Failed to fetch trend data");
})
.finally(() => {
if (!controller.signal.aborted) {

View File

@@ -7,6 +7,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchBans } from "../api/dashboard";
import { handleFetchError } from "../utils/fetchError";
import type { DashboardBanItem, TimeRange, BanOriginFilter } from "../types/ban";
/** Items per page for the ban table. */
@@ -63,7 +64,7 @@ export function useBans(
setBanItems(data.items);
setTotal(data.total);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch data");
handleFetchError(err, setError, "Failed to fetch bans");
} finally {
setLoading(false);
}

View File

@@ -14,12 +14,14 @@ import {
updateBlocklist,
updateSchedule,
} from "../api/blocklist";
import { handleFetchError } from "../utils/fetchError";
import type {
BlocklistSource,
BlocklistSourceCreate,
BlocklistSourceUpdate,
ImportLogListResponse,
ImportRunResult,
PreviewResponse,
ScheduleConfig,
ScheduleInfo,
} from "../types/blocklist";
@@ -65,7 +67,7 @@ export function useBlocklists(): UseBlocklistsReturn {
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
setError(err instanceof Error ? err.message : "Failed to load blocklists");
handleFetchError(err, setError, "Failed to load blocklists");
setLoading(false);
}
});
@@ -144,7 +146,7 @@ export function useSchedule(): UseScheduleReturn {
setLoading(false);
})
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : "Failed to load schedule");
handleFetchError(err, setError, "Failed to load schedule");
setLoading(false);
});
}, []);
@@ -200,7 +202,7 @@ export function useImportLog(
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
setError(err instanceof Error ? err.message : "Failed to load import log");
handleFetchError(err, setError, "Failed to load import log");
setLoading(false);
}
});
@@ -242,7 +244,7 @@ export function useRunImport(): UseRunImportReturn {
const result = await runImportNow();
setLastResult(result);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Import failed");
handleFetchError(err, setError, "Import failed");
} finally {
setRunning(false);
}

View File

@@ -12,11 +12,13 @@ import {
flushLogs,
previewLog,
reloadConfig,
restartFail2Ban,
testRegex,
updateGlobalConfig,
updateJailConfig,
updateServerSettings,
} from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type {
AddLogPathRequest,
GlobalConfig,
@@ -65,9 +67,7 @@ export function useJailConfigs(): UseJailConfigsResult {
setTotal(resp.total);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch jail configs");
})
.finally(() => {
setLoading(false);
@@ -128,9 +128,7 @@ export function useJailConfigDetail(name: string): UseJailConfigDetailResult {
setJail(resp.jail);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch jail config");
})
.finally(() => {
setLoading(false);
@@ -191,9 +189,7 @@ export function useGlobalConfig(): UseGlobalConfigResult {
fetchGlobalConfig()
.then(setConfig)
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch global config");
})
.finally(() => {
setLoading(false);
@@ -251,9 +247,7 @@ export function useServerSettings(): UseServerSettingsResult {
setSettings(resp.settings);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch server settings");
})
.finally(() => {
setLoading(false);

View File

@@ -13,6 +13,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchJails } from "../api/jails";
import { fetchJailConfigs } from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type { JailConfig } from "../types/config";
import type { JailSummary } from "../types/jail";
@@ -110,7 +111,7 @@ export function useConfigActiveStatus(): UseConfigActiveStatusResult {
})
.catch((err: unknown) => {
if (ctrl.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to load status.");
handleFetchError(err, setError, "Failed to load active status.");
setLoading(false);
});
}, []);

View File

@@ -2,6 +2,7 @@
* Generic config hook for loading and saving a single entity.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { handleFetchError } from "../utils/fetchError";
export interface UseConfigItemResult<T, U> {
data: T | null;
@@ -46,7 +47,7 @@ export function useConfigItem<T, U>(
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to load data");
handleFetchError(err, setError, "Failed to load data");
setLoading(false);
});
}, [fetchFn]);

View File

@@ -9,6 +9,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchBansByCountry } from "../api/map";
import { handleFetchError } from "../utils/fetchError";
import type { DashboardBanItem, BanOriginFilter, TimeRange } from "../types/ban";
// ---------------------------------------------------------------------------
@@ -77,7 +78,7 @@ export function useDashboardCountryData(
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to fetch data");
handleFetchError(err, setError, "Failed to fetch dashboard country data");
})
.finally(() => {
if (!controller.signal.aborted) {

View File

@@ -4,6 +4,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchHistory, fetchIpHistory } from "../api/history";
import { handleFetchError } from "../utils/fetchError";
import type {
HistoryBanItem,
HistoryQuery,
@@ -44,9 +45,7 @@ export function useHistory(query: HistoryQuery = {}): UseHistoryResult {
setTotal(resp.total);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch history");
})
.finally((): void => {
setLoading(false);
@@ -91,9 +90,7 @@ export function useIpHistory(ip: string): UseIpHistoryResult {
setDetail(resp);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch IP history");
})
.finally((): void => {
setLoading(false);

View File

@@ -7,6 +7,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchBansByJail } from "../api/dashboard";
import { handleFetchError } from "../utils/fetchError";
import type { BanOriginFilter, JailBanCount, TimeRange } from "../types/ban";
// ---------------------------------------------------------------------------
@@ -65,9 +66,7 @@ export function useJailDistribution(
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(
err instanceof Error ? err.message : "Failed to fetch jail distribution",
);
handleFetchError(err, setError, "Failed to fetch jail distribution");
})
.finally(() => {
if (!controller.signal.aborted) {

View File

@@ -7,6 +7,7 @@
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { handleFetchError } from "../utils/fetchError";
import {
addIgnoreIp,
banIp,
@@ -92,7 +93,7 @@ export function useJails(): UseJailsResult {
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
setError(err instanceof Error ? err.message : String(err));
handleFetchError(err, setError, "Failed to load jails");
}
})
.finally(() => {
@@ -195,7 +196,7 @@ export function useJailDetail(name: string): UseJailDetailResult {
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
setError(err instanceof Error ? err.message : String(err));
handleFetchError(err, setError, "Failed to fetch jail detail");
}
})
.finally(() => {
@@ -309,7 +310,7 @@ export function useJailBannedIps(jailName: string): UseJailBannedIpsResult {
setItems(resp.items);
setTotal(resp.total);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : String(err));
handleFetchError(err, setError, "Failed to fetch jailed IPs");
} finally {
setLoading(false);
}
@@ -415,7 +416,7 @@ export function useActiveBans(): UseActiveBansResult {
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
setError(err instanceof Error ? err.message : String(err));
handleFetchError(err, setError, "Failed to fetch active bans");
}
})
.finally(() => {
@@ -496,7 +497,7 @@ export function useIpLookup(): UseIpLookupResult {
setResult(res);
})
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : String(err));
handleFetchError(err, setError, "Failed to lookup IP");
})
.finally(() => {
setLoading(false);

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { fetchMapColorThresholds, updateMapColorThresholds } from "../api/config";
import { handleFetchError } from "../utils/fetchError";
import type {
MapColorThresholdsResponse,
MapColorThresholdsUpdate,
@@ -26,7 +27,7 @@ export function useMapColorThresholds(): UseMapColorThresholdsResult {
const data = await fetchMapColorThresholds();
setThresholds(data);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch map color thresholds");
handleFetchError(err, setError, "Failed to fetch map color thresholds");
} finally {
setLoading(false);
}

View File

@@ -4,6 +4,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchBansByCountry } from "../api/map";
import { handleFetchError } from "../utils/fetchError";
import type { BansByCountryResponse, MapBanItem, TimeRange } from "../types/map";
import type { BanOriginFilter } from "../types/ban";
@@ -68,9 +69,7 @@ export function useMapData(
setData(resp);
})
.catch((err: unknown) => {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
handleFetchError(err, setError, "Failed to fetch map data");
})
.finally((): void => {
setLoading(false);

View File

@@ -8,6 +8,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchServerStatus } from "../api/dashboard";
import { handleFetchError } from "../utils/fetchError";
import type { ServerStatus } from "../types/server";
/** How often to poll the status endpoint (milliseconds). */
@@ -49,7 +50,7 @@ export function useServerStatus(): UseServerStatusResult {
setBanguiVersion(data.bangui_version);
setError(null);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch server status");
handleFetchError(err, setError, "Failed to fetch server status");
} finally {
setLoading(false);
}

View File

@@ -6,6 +6,7 @@
import { useCallback, useEffect, useState } from "react";
import { ApiError } from "../api/client";
import { handleFetchError } from "../utils/fetchError";
import { getSetupStatus, submitSetup } from "../api/setup";
import type {
SetupRequest,
@@ -44,9 +45,11 @@ export function useSetup(): UseSetupResult {
const resp = await getSetupStatus();
setStatus(resp);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "Failed to fetch setup status";
console.warn("Setup status check failed:", errorMessage);
setError(errorMessage);
const fallback = "Failed to fetch setup status";
handleFetchError(err, setError, fallback);
if (!(err instanceof DOMException && err.name === "AbortError")) {
console.warn("Setup status check failed:", err instanceof Error ? err.message : fallback);
}
} finally {
setLoading(false);
}

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { fetchTimezone } from "../api/setup";
import { handleFetchError } from "../utils/fetchError";
export interface UseTimezoneDataResult {
timezone: string;
@@ -21,7 +22,7 @@ export function useTimezoneData(): UseTimezoneDataResult {
const resp = await fetchTimezone();
setTimezone(resp.timezone);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch timezone");
handleFetchError(err, setError, "Failed to fetch timezone");
setTimezone("UTC");
} finally {
setLoading(false);

View File

@@ -0,0 +1,14 @@
/**
* Normalize fetch error handling across hooks.
*/
export function handleFetchError(
err: unknown,
setError: (value: string | null) => void,
fallback: string = "Unknown error",
): void {
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
setError(err instanceof Error ? err.message : fallback);
}