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/.failingall present; thrown value is a realErrorinstance; unparseable/body-less error response → generic message, no crash; 2xx success path unaffected. BerakhirnyaReviewPage.test.tsx's submit-422 test swapped to reject withObject.assign(new Error("Validasi gagal"), { code, failing })— a real apiFetch-shaped error — instead of a raw mock object, so the(err as {failing?})?.failingbranch is now exercised for real rather than mock-only.- Peleburan/Pembubaran review page tests left unchanged (they mock the
api-clientmodule 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 inpdf-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.