From 7d94a15ca3ca3d6b7dfbf5dae94e8d80f3685a3f Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Mon, 3 Aug 2026 18:30:52 +0500 Subject: [PATCH] handle backend errors --- src/components/auth/signup-form.tsx | 2 +- src/lib/api-client.ts | 19 +++++++++++++++++++ src/queries/islands.ts | 7 ++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx index 6f974fc..f42204e 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -188,7 +188,7 @@ export default function SignUpForm() { Atolls - {atolls?.data.map((atoll) => ( + {(atolls?.data ?? []).map((atoll) => ( {atoll.name} diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 5c89bbc..d26c684 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -29,6 +29,25 @@ apiClient.interceptors.request.use((config) => { return config; }); +// Normalise non-JSON error pages. Django (DisallowedHost / DEBUG tracebacks), +// nginx (502/504), etc. return an HTML body on 4xx/5xx instead of the JSON +// envelope callers expect. Left as-is, the raw HTML string flows into consumers +// and crashes things like `atolls.data.map(...)`. Rewrite it into a JSON error +// body so `handleApiResponse` and action code read a clean `{ message, detail }`. +apiClient.interceptors.response.use((response) => { + const contentType = String(response.headers?.["content-type"] ?? ""); + const isHtml = !contentType.includes("application/json"); + if (response.status >= 400 && isHtml && typeof response.data === "string") { + const title = response.data.match(/([\s\S]*?)<\/title>/i)?.[1]?.trim(); + response.data = { + message: `Server error (${response.status})`, + detail: + title || `The server returned a non-JSON ${response.status} response.`, + }; + } + return response; +}); + // On 401 the token is dead — clear it and bounce to signin (once). apiClient.interceptors.response.use((response) => { if (response.status === 401) { diff --git a/src/queries/islands.ts b/src/queries/islands.ts index 5f7d873..382de8c 100644 --- a/src/queries/islands.ts +++ b/src/queries/islands.ts @@ -1,12 +1,17 @@ import type { ApiResponse, Atoll } from "@/lib/backend-types"; import apiClient from "@/lib/api-client"; +import { handleApiResponse } from "@/utils/tryCatch"; /** * Atoll list (with nested islands) for the signup atoll/island dropdowns. * The endpoint is paginated, so this returns the `{ meta, links, data }` * envelope — consumers read `.data`. + * + * Runs through `handleApiResponse` so a 4xx/5xx (e.g. DisallowedHost 400) + * throws a clean error for React Query to surface, instead of returning a + * non-envelope body that crashes `atolls.data.map(...)`. */ export async function getAtolls() { const response = await apiClient.get("/api/auth/atolls/"); - return response.data as ApiResponse<Atoll>; + return handleApiResponse<ApiResponse<Atoll>>(response, "getAtolls"); }