Repository navigation
Commit 8a83ceb
docs(spec): IHttpRequest.body TSDoc states what a handler reads and that the read may refuse (#24068)
Fixes #24043
Clause-②: no
## What
`IHttpRequest.body`'s TSDoc in
`packages/spec/src/contracts/http-server.ts` now says what a handler
reads and what a read can do. It used to say only "Request body (parsed
— JSON or form data when applicable)".
- **What it is:** the parsed JSON document for `application/json`, or
the parsed fields of a `multipart/form-data` /
`application/x-www-form-urlencoded` form. `{}` when the request carries
no body.
- **It may refuse:** an implementation that cannot parse the body, or
does not read its media type, throws from the read instead of handing
over an invented value. The standard transport
(`@objectstack/plugin-hono-server`) does this and answers 400
`INVALID_REQUEST` itself, replacing whatever the handler wrote. So a
handler must not catch the refusal and carry on.
- **Raw bytes:** a handler that needs them reads `rawBody()` and never
reads `body`.
TSDoc only. The type (`body?: any`), every other member and every
runtime path are unchanged. `rawBody`'s TSDoc is unchanged too, because
it does not contradict the new text (see Acceptance notes). There is one
prose-only `patch` changeset for `@objectstack/spec`, because the TSDoc
ships in the `.d.ts`.
## Measured: each clause against the standard transport (`origin/main`
`e246f06119`)
| Clause | Code | Pin in `request-body-refusal.test.ts` |
|:---|:---|:---|
| JSON is parsed; empty or whitespace-only JSON is `{}` |
`request-body.ts` `readRequestBody`, the `application/json` arm
(case-insensitive) | "a valid JSON body reaches the handler parsed" ·
"an empty or whitespace-only JSON body is `{}`" · "a declared JSON media
type is matched case-insensitively" |
| A form is parsed | the `multipart/form-data` /
`application/x-www-form-urlencoded` arm (`parseBody()`) | "a urlencoded
form body reaches the handler parsed" |
| `{}` when there is no body | the last arm: `declaresBody` (RFC 9112
framing headers) is false, so the answer is `{ body: {} }` | "a request
with no body is `{}`, whatever it declares" · "CONTROL over the wire: a
POST with no body is `{}`" |
| Refuses a body that does not parse | the JSON arm's `JSON.parse`
catch, and the form arm's catch, both answer `RequestBodyRefusal` (400
`INVALID_REQUEST`) | steps 2, 3, 5 and 6 · "a form body that does not
parse is refused" |
| Refuses a non-empty body in a media type it does not read | the last
arm: `declaresBody` is true, so the answer is a refusal (`text/plain`,
`application/octet-stream`, no Content-Type, …) | "step 4 — a valid
query body sent as text/plain" · "a body with no Content-Type at all is
refused" · the step 4 test over the wire |
| The refusal is thrown from the read, and the transport answers it
whatever the handler wrote | `adapter.ts` `runHandler`: the `get body()`
accessor throws and records `refusedBodyRead`; after the handler
settles, `if (refusedBodyRead)` returns the declared envelope in place
of the handler's response | the query door catches the throw and writes
its own 500, and the pin still sees 400 `INVALID_REQUEST` ("step 2" …
"step 4") |
| `rawBody()` is the byte door | the `rawBody` member reads
`c.req.arrayBuffer()`; JSON is read with `text()`, which fills the same
Hono body cache | the module's CONTROL list: the byte doors still answer
as before |
The handler consequence from the card holds in this form: the handler's
response is discarded, not written after the 400. What survives the
catch is any side effect the handler performs. So the TSDoc tells a
handler not to catch the refusal and carry on, and invents no API to go
with it.
## What ships
`pnpm --filter @objectstack/spec build` (exit 0). `grep -rl 'must not
catch the refusal and carry on' dist` matches
`dist/contracts/index.d.ts` and `dist/contracts/index.d.mts`. Positive
control: `Raw body accessor for binary uploads` (the untouched `rawBody`
text) matches the same two files. The old sentence `parsed — JSON or
form data when applicable` matches nothing in `dist`. `dist` is in
`files[]`.
## Gates (head `2a55ea38`; same tree as `8026677d`, where they ran; only
the commit message was reworded)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 78 commands. All 78 were run, each
exit code captured before any pipe, plus the 5 roster families keeping a
roster under one of this diff's directories (`check-changeset-fixed`,
`check:meta-url-spelling`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`): **79 exit 0, 4
exit 3**.
- Exit 3 means PREREQUISITE NOT MET, so these four are NOT MEASURED
locally: `check-plugin-teardown-shape --self-test` (shallow clone cannot
reach its pinned control commit), `@objectstack/lint
check:doc-formula-expressions` (`@objectstack/formula` not built),
`check:dual-build-cjs-loads` and `check:lean-entry-closure` (workspace
`dist` not built). None of them reads `http-server.ts` or a changeset.
CI runs them.
- `--ran` reconciliation: "78 derived famil(ies) accounted for — 74 run,
4 NOT-MEASURED (4 DERIVED from a recorded exit 3)".
- `pnpm --filter @objectstack/spec typecheck` exit 0. `pnpm --filter
@objectstack/spec check:generated` exit 0. ESLint (`--no-inline-config`)
over both changed files: exit 0 (the `.md` file is ignored by the
config).
## Acceptance notes
- **Other producers of `IHttpRequest`, measured on `main`** (none is
changed here; the text says what an implementation MAY do and does not
claim every producer throws):
- `packages/qa/http-conformance/src/adapter.ts` reads JSON, urlencoded
and text bodies eagerly. A malformed JSON body becomes `{}` (`catch {
body = {}; }`), a `text/*` body becomes the string, and every other
media type is `{}` with the bytes left for `rawBody()`. It does not
refuse.
- `packages/rest/src/http-request-test-builder.ts` leaves `body` and
`rawBody` absent unless the caller passes them, so `body` is `undefined`
rather than `{}` for a fixture that states none. The member stays
optional (`body?`), and no type change was in scope.
- **`rawBody` TSDoc ("consume the underlying request stream lazily so
handlers that only need `body` do not pay the parsing cost").** This
still matches the standard adapter: `rawBody()` reads
`c.req.arrayBuffer()` only when called, and the media types `body` does
not read stay unread until then. JSON and form bodies are read before
the handler, which was also true before PR #24005. It does not
contradict the new `body` text, so it is unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01KNNm3SRPyw288E5DT4qVGx
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNNm3SRPyw288E5DT4qVGx)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8133cc7 commit 8a83ceb
2 files changed
Lines changed: 29 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
24 | 40 | | |
25 | 41 | | |
26 | 42 | | |
| |||
0 commit comments