Repository navigation
docs(spec): IHttpRequest.body TSDoc states what a handler reads and that the read may refuse - #24068
Conversation
…hat the read may refuse
The parsed JSON or form body, `{}` when there is none; a transport may
throw from the read for a body it cannot parse or a media type it does
not read, and the standard transport answers 400 INVALID_REQUEST itself;
a handler that needs the bytes reads rawBody(). TSDoc only, no type change.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNNm3SRPyw288E5DT4qVGx
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7d5c29c02ce2add07f1ebddd00291b3d208430d3 && git checkout 7d5c29c02ce2add07f1ebddd00291b3d208430d3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 077b75f5593d813aa5bd1c6d51a3fb19a7d6322f 2a55ea381bfeda6b6b3cba3c7520c251a6222aa0 && git checkout -B drift-repro 077b75f5593d813aa5bd1c6d51a3fb19a7d6322f && git merge --no-ff 2a55ea381bfeda6b6b3cba3c7520c251a6222aa0
node scripts/docs-audit/affected-docs.mjs --json 077b75f5593d813aa5bd1c6d51a3fb19a7d6322f |
Contract reviewServed-tier: Net diff against the merge base ① Derived judgmentsEach sentence the diff adds to S1 — "Request body, parsed by its declared content type: the parsed JSON document for S2 — " S3 — "Reading it may REFUSE: an implementation that cannot parse the body, or that does not read the body's media type, throws from the read rather than hand over an invented value." Right as permission, by its lead. The clause after the colon defines what a refusal is, and its three arms match the code: an unreadable or malformed declared-JSON body ( S4 — "The standard transport ( S5 — "so a handler must not catch the refusal and carry on, because anything it does after the catch acts on a request that has already been refused." Right. The refusal is recorded at the moment of the read ( S6 — "A handler that needs the raw bytes (a file upload, a media type not listed above) reads {@link rawBody} and never reads Type and member check. The hunk is +17/−1 inside the TSDoc block; Changeset sentences. "What it is" restates S1 and S2 — right. "It may refuse" restates S3, S4 and S5, including "So a handler must not catch the refusal and carry on" — right, under the same MAY reading. "Raw bytes … reads PR body. Every pin the measured table names exists under that name in ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24068 @
|
Fixes #24043
Clause-②: no
What
IHttpRequest.body's TSDoc inpackages/spec/src/contracts/http-server.tsnow 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)".application/json, or the parsed fields of amultipart/form-data/application/x-www-form-urlencodedform.{}when the request carries no body.@objectstack/plugin-hono-server) does this and answers 400INVALID_REQUESTitself, replacing whatever the handler wrote. So a handler must not catch the refusal and carry on.rawBody()and never readsbody.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-onlypatchchangeset for@objectstack/spec, because the TSDoc ships in the.d.ts.Measured: each clause against the standard transport (
origin/maine246f06119)request-body-refusal.test.ts{}request-body.tsreadRequestBody, theapplication/jsonarm (case-insensitive){}" · "a declared JSON media type is matched case-insensitively"multipart/form-data/application/x-www-form-urlencodedarm (parseBody()){}when there is no bodydeclaresBody(RFC 9112 framing headers) is false, so the answer is{ body: {} }{}, whatever it declares" · "CONTROL over the wire: a POST with no body is{}"JSON.parsecatch, and the form arm's catch, both answerRequestBodyRefusal(400INVALID_REQUEST)declaresBodyis true, so the answer is a refusal (text/plain,application/octet-stream, no Content-Type, …)adapter.tsrunHandler: theget body()accessor throws and recordsrefusedBodyRead; after the handler settles,if (refusedBodyRead)returns the declared envelope in place of the handler's responseINVALID_REQUEST("step 2" … "step 4")rawBody()is the byte doorrawBodymember readsc.req.arrayBuffer(); JSON is read withtext(), which fills the same Hono body cacheThe 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' distmatchesdist/contracts/index.d.tsanddist/contracts/index.d.mts. Positive control:Raw body accessor for binary uploads(the untouchedrawBodytext) matches the same two files. The old sentenceparsed — JSON or form data when applicablematches nothing indist.distis infiles[].Gates (head
2a55ea38; same tree as8026677d, where they ran; only the commit message was reworded)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 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.check-plugin-teardown-shape --self-test(shallow clone cannot reach its pinned control commit),@objectstack/lint check:doc-formula-expressions(@objectstack/formulanot built),check:dual-build-cjs-loadsandcheck:lean-entry-closure(workspacedistnot built). None of them readshttp-server.tsor a changeset. CI runs them.--ranreconciliation: "78 derived famil(ies) accounted for — 74 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)".pnpm --filter @objectstack/spec typecheckexit 0.pnpm --filter @objectstack/spec check:generatedexit 0. ESLint (--no-inline-config) over both changed files: exit 0 (the.mdfile is ignored by the config).Acceptance notes
IHttpRequest, measured onmain(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.tsreads JSON, urlencoded and text bodies eagerly. A malformed JSON body becomes{}(catch { body = {}; }), atext/*body becomes the string, and every other media type is{}with the bytes left forrawBody(). It does not refuse.packages/rest/src/http-request-test-builder.tsleavesbodyandrawBodyabsent unless the caller passes them, sobodyisundefinedrather than{}for a fixture that states none. The member stays optional (body?), and no type change was in scope.rawBodyTSDoc ("consume the underlying request stream lazily so handlers that only needbodydo not pay the parsing cost"). This still matches the standard adapter:rawBody()readsc.req.arrayBuffer()only when called, and the media typesbodydoes not read stay unread until then. JSON and form bodies are read before the handler, which was also true before PR fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005. It does not contradict the newbodytext, so it is unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_01KNNm3SRPyw288E5DT4qVGx
Generated by Claude Code