Stirling-Tools/Stirling-PDF · error · HttpError
${status} ${statusText}
Error message
${status} ${statusText} What it means
The generic HttpError thrown by unwrap<T>() for ANY non-2xx response. HttpError is a named class (status, statusText, body) whose message is `${status} ${statusText}`. unwrap() first attempts to parse the response body as JSON (capturing it as error.body for ProblemDetail-shaped payloads), swallowing parse failures. It is the shared handler used by saasJson (and indirectly localJson-style calls), so the same message covers 4xx and 5xx alike.
Source
Thrown at frontend/editor/src/portal/api/http.ts:133
} | null;
return body?.detail ?? body?.message ?? body?.error ?? error.message;
}
return error instanceof Error ? error.message : String(error);
}
// ────────────────────────────────────────────────────────────────────────────
// Shared response handler
// ────────────────────────────────────────────────────────────────────────────
async function unwrap<T>(res: Response): Promise<T> {
if (!res.ok) {
let body: unknown = null;
try {
body = await res.json();
} catch {
// ignore — non-JSON error response
}
throw new HttpError(res.status, res.statusText, body);
}
// 204 / empty-body responses have nothing to parse.
if (res.status === 204 || res.headers.get("Content-Length") === "0") {
return undefined as T;
}
const text = await res.text();
return (text ? JSON.parse(text) : undefined) as T;
}
// ────────────────────────────────────────────────────────────────────────────
// local — this instance's backend, via the localBackend seam (base URL + auth).
// Self-hosted: same-origin + Spring bearer. SaaS: the SaaS backend + Supabase JWT.
// ────────────────────────────────────────────────────────────────────────────
async function localJson<T>(
path: string,
options: HttpRequestOptions = {},
): Promise<T> {View on GitHub (pinned to 9ef20dcab8)
Solutions
- Catch HttpError by class and read .status and .body (ProblemDetail's detail/message/error) — use errorMessage(error) from this module which already unwraps those fields.
- If 401/403, refresh the token/session via the appropriate auth seam (onLocalUnauthorized / portalSaasSession) before retrying.
- If 404, confirm the endpoint shipped on the backend (Mocks=off hits the real backend and 404s until the route exists).
- For 400, inspect error.body for the field-level validation message and surface it to the user.
Example fix
// before — caller sees only `${status} ${statusText}`
try { await apiClient.saas.json('/api/v1/payg/wallet'); }
catch (e) { console.log(e.message); // "400 Bad Request" — no detail }
// after — use the module's errorMessage() helper to unwrap the ProblemDetail body
import { HttpError, errorMessage } from "@portal/api/http";
try { await apiClient.saas.json('/api/v1/payg/wallet'); }
catch (e) {
if (e instanceof HttpError) console.log(e.status, errorMessage(e)); // 400, "wallet id required"
} Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
import { HttpError } from "@portal/api/http";
function isHttpError(e: unknown): e is HttpError { return e instanceof HttpError; }
function isHttp(e: unknown, status?: number): e is HttpError {
return e instanceof HttpError && (status === undefined || e.status === status);
} Try / catch
import { HttpError, errorMessage } from "@portal/api/http";
try {
const data = await apiClient.saas.json('/api/v1/payg/wallet');
} catch (e) {
if (e instanceof HttpError) {
if (e.status === 401) { await refreshSession(); return; }
notify(errorMessage(e)); // unwraps ProblemDetail detail/message/error
return;
}
throw e;
} Prevention
- Always catch by HttpError class and read .status + .body.
- Use errorMessage() to unwrap ProblemDetail bodies consistently.
- Handle 401 with a token refresh before retrying.
When it happens
Trigger: Any fetch routed through unwrap() returns a non-ok status: server-side validation failure (400), auth failure (401/403 on paths without a dedicated handler), not-found (404), or server error (500). The body is parsed if JSON, else null.
Common situations: Backend returned a Spring ProblemDetail (JSON) for a bad request; an endpoint was not yet implemented (404 with Mocks=off, per the module header comment); the bearer/JWT expired producing a 401; a 500 from an unhandled server exception.
Related errors
- SaaS request failed (${res.status})
- SaaS API not configured — set VITE_SAAS_API_URL to enable po
- No SaaS session — admin must link an account before attended
- No SaaS session
- Member must belong to a team to become its owner
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/ef757b88a38708ae.
Report an issue: GitHub.