think
16px
820px

apiFetch error-body preservation fix

Status

Done.

Commit

f569356dd692d0e0ef4c85ce0dde557a09656ffb — fix(frontend): apiFetch preserves error body (code/failing) on thrown errors — revives the inline-422 branch across staged flows

Fix shape

Object.assign, not a dedicated ApiError class. In frontend/src/lib/api-client.ts:

if (!res.ok) {
  const body = await res.json().catch(() => ({ error: res.statusText }));
  const err = new Error(body?.error ?? "Request failed");
  Object.assign(err, body);
  throw err;
}

err.message semantics are unchanged (still body.error ?? "Request failed"); err remains instanceof Error so every err instanceof Error ? err.message : ... consumer across the app (grepped ~20 call sites) is unaffected. code, failing, and any other parsed-body fields now ride along as own properties.

Tests

  • New frontend/src/lib/__tests__/api-client.test.ts (4 cases, fetch-mocked): structured 422 body → .message/.code/.failing all present; thrown value is a real Error instance; unparseable/body-less error response → generic message, no crash; 2xx success path unaffected.
  • BerakhirnyaReviewPage.test.tsx's submit-422 test swapped to reject with Object.assign(new Error("Validasi gagal"), { code, failing }) — a real apiFetch-shaped error — instead of a raw mock object, so the (err as {failing?})?.failing branch is now exercised for real rather than mock-only.
  • Peleburan/Pembubaran review page tests left unchanged (they mock the api-client module entirely; unaffected by design).

Verification

  • bunx tsc --noEmit — clean.
  • bunx vitest run src/lib/ src/pages/__tests__/PeleburanReviewPage.test.tsx src/pages/__tests__/PembubaranReviewPage.test.tsx src/pages/__tests__/BerakhirnyaReviewPage.test.tsx — 23 files / 142 tests, all passed.
  • Full bunx vitest run — 516 passed; 1 pre-existing unrelated failure (PerbaikanReviewPage.test.tsx, DOMMatrix is not defined — a jsdom/pdf.js environment gap in pdf-viewer.tsx, not touched by this change).

Concerns

None blocking. Note for later: if a non-2xx response body is ever a bare JSON primitive (string/number) rather than an object, Object.assign(err, body) would silently no-op (null/undefined sources are ignored by Object.assign, but a string source copies its indices as properties) — not currently possible given the backend always returns {error, code?, ...} objects on error, so left unguarded to keep the diff minimal per the "minimal AppError" instruction.