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

  1. 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.
  2. If 401/403, refresh the token/session via the appropriate auth seam (onLocalUnauthorized / portalSaasSession) before retrying.
  3. If 404, confirm the endpoint shipped on the backend (Mocks=off hits the real backend and 404s until the route exists).
  4. 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

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


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/ef757b88a38708ae. Report an issue: GitHub.