{"record":{"id":"ef757b88a38708ae","repo":"Stirling-Tools/Stirling-PDF","slug":"status-statustext","errorCode":null,"errorMessage":"${status} ${statusText}","messagePattern":"\\$\\{status\\} \\$\\{statusText\\}","errorType":"http","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/api/http.ts","lineNumber":133,"sourceCode":"    } | null;\n    return body?.detail ?? body?.message ?? body?.error ?? error.message;\n  }\n  return error instanceof Error ? error.message : String(error);\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Shared response handler\n// ────────────────────────────────────────────────────────────────────────────\n\nasync function unwrap<T>(res: Response): Promise<T> {\n  if (!res.ok) {\n    let body: unknown = null;\n    try {\n      body = await res.json();\n    } catch {\n      // ignore — non-JSON error response\n    }\n    throw new HttpError(res.status, res.statusText, body);\n  }\n  // 204 / empty-body responses have nothing to parse.\n  if (res.status === 204 || res.headers.get(\"Content-Length\") === \"0\") {\n    return undefined as T;\n  }\n  const text = await res.text();\n  return (text ? JSON.parse(text) : undefined) as T;\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// local — this instance's backend, via the localBackend seam (base URL + auth).\n// Self-hosted: same-origin + Spring bearer. SaaS: the SaaS backend + Supabase JWT.\n// ────────────────────────────────────────────────────────────────────────────\n\nasync function localJson<T>(\n  path: string,\n  options: HttpRequestOptions = {},\n): Promise<T> {","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/api/http.ts#L115-L151","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — caller sees only `${status} ${statusText}`\ntry { await apiClient.saas.json('/api/v1/payg/wallet'); }\ncatch (e) { console.log(e.message); // \"400 Bad Request\" — no detail }\n\n// after — use the module's errorMessage() helper to unwrap the ProblemDetail body\nimport { HttpError, errorMessage } from \"@portal/api/http\";\ntry { await apiClient.saas.json('/api/v1/payg/wallet'); }\ncatch (e) {\n  if (e instanceof HttpError) console.log(e.status, errorMessage(e)); // 400, \"wallet id required\"\n}","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"import { HttpError } from \"@portal/api/http\";\nfunction isHttpError(e: unknown): e is HttpError { return e instanceof HttpError; }\nfunction isHttp(e: unknown, status?: number): e is HttpError {\n  return e instanceof HttpError && (status === undefined || e.status === status);\n}","tryCatchPattern":"import { HttpError, errorMessage } from \"@portal/api/http\";\ntry {\n  const data = await apiClient.saas.json('/api/v1/payg/wallet');\n} catch (e) {\n  if (e instanceof HttpError) {\n    if (e.status === 401) { await refreshSession(); return; }\n    notify(errorMessage(e)); // unwraps ProblemDetail detail/message/error\n    return;\n  }\n  throw e;\n}","preventionTips":["Always catch by HttpError class and read .status + .body.","Use errorMessage() to unwrap ProblemDetail bodies consistently.","Handle 401 with a token refresh before retrying."],"tags":["portal","http","http-error","response-handling","typescript"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}