Commit 6f172d3
authored
fix(web): reject 2xx responses whose body is not JSON (#194)
## Summary
- `readJson` no longer collapses a failed JSON parse into `null` typed
as the payload
- a 2xx response whose body has bytes that are not JSON now raises
`ApiError` with the status
- the tolerant paths that were intentional stay: non-JSON error bodies
still fall back to the status message, and an empty 2xx body still
resolves to `null`
## Why
```ts
const payload = (await response.json().catch(() => null)) as unknown;
if (!response.ok) { throw new ApiError(...); }
return payload as T; // null, typed as T
```
The `catch(() => null)` exists so that an error response carrying HTML
or an empty body still produces a usable `ApiError`. But it applies to
successful responses too, so any transport-level corruption under a 200
is handed back as `null` wearing the payload's type. Nothing fails at
the boundary; the app crashes later, wherever a caller first reads a
property.
That is not hypothetical. #193 fixes a Cloudflare deployment where the
Workers runtime re-encoded an already-gzipped body, so
`/api/auth/session` returned 200 with `1f 8b` bytes. The symptom
operators actually saw was a `TypeError` reading `authenticated` —
several frames away from the cause, with a green network tab.
This is independent of #193 and does not overlap with it: that PR stops
producing the bad body, this one stops the client from disguising a bad
body as a valid payload.
## Notes
- The body is read once via `response.text()` and parsed locally, so the
2xx and non-2xx paths can differ. `parseJson` returns `undefined` for a
non-JSON body, which `JSON.parse` can never produce for a valid one.
- No `/api/*` endpoint returns an empty 2xx body today (every `DELETE`
handler responds with JSON), but empty bodies keep resolving to `null`
so a future `204` is not a behavior change.
## Validation
- `npx vitest run` — 57 files / 528 tests pass
- new `web/src/api.test.ts` covers all five paths; the malformed-2xx
case fails against the previous implementation (`expected null to be an
instance of ApiError`)
- `npm run lint`, `oxfmt --check .`, `tsc -p src/tsconfig.json
--noEmit`, and `tsc -p web/tsconfig.json --noEmit` are clean1 parent 575a992 commit 6f172d3
2 files changed
Lines changed: 70 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
72 | 79 | | |
73 | 80 | | |
74 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
75 | 94 | | |
76 | 95 | | |
77 | 96 | | |
| |||
0 commit comments