Skip to content

docs(spec): IHttpRequest.body TSDoc states what a handler reads and that the read may refuse - #24068

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24043-http-request-body-tsdoc
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24043-http-request-body-tsdoc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 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 new body text, so it is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KNNm3SRPyw288E5DT4qVGx


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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): node scripts/docs-audit/affected-docs.mjs --json 077b75f5593d813aa5bd1c6d51a3fb19a7d6322f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7d5c29c02ce2add07f1ebddd00291b3d208430d3 — the merge of head 2a55ea381bfeda6b6b3cba3c7520c251a6222aa0 into base 077b75f5593d813aa5bd1c6d51a3fb19a7d6322f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 16, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2a55ea381bfeda6b6b3cba3c7520c251a6222aa0
Local-runs: none
Scope: packages/spec/src/contracts/http-server.ts + packages/plugins/plugin-hono-server/src/request-body.ts, packages/plugins/plugin-hono-server/src/adapter.ts, packages/plugins/plugin-hono-server/src/request-body-refusal.test.ts, packages/qa/http-conformance/src/adapter.ts, packages/rest/src/http-request-test-builder.ts, packages/runtime/src/dispatcher-plugin.ts, packages/spec/package.json, packages/spec/tsup.config.ts, pnpm-lock.yaml (the hono resolution), and hono 4.13.12's dist/request.js, dist/context.js and dist/utils/body.js read from the pnpm store; PR #24005's diff and its contract review 6222965551. The changeset .changeset/24043-http-request-body-tsdoc.md is a prose face, judged in ②.

Net diff against the merge base e246f0611: 2 files, 29 insertions, 1 deletion; one commit. The five cited code files are byte-identical at the merge base, at the current origin/main and at the head, so one reading covers all three. No governed path (Governed Surface Queue Guard green); head repo is the base repo; size/s; the card's Claim: names this branch and the claim guards are green.

① Derived judgments

Each sentence the diff adds to IHttpRequest.body's TSDoc (http-server.ts:23-39), against the standard transport on main.

S1 — "Request body, parsed by its declared content type: the parsed JSON document for application/json, or the parsed fields of a form (multipart/form-data, application/x-www-form-urlencoded)." Right. readRequestBody parses exactly three media types: application/json (request-body.ts:136, includes on the lowered header, so parameters and case are tolerated) and the two form types through parseBody() (:153-156); every other media type is left unread (:162-166). A +json structured-syntax suffix (application/vnd.api+json) is NOT parsed, so naming application/json alone is exact, not an under-claim. Pinned: "a valid JSON body reaches the handler parsed", "a declared JSON media type is matched case-insensitively", "a urlencoded form body reaches the handler parsed". The multipart PARSE has no CONTROL pin (only the malformed-multipart refusal is pinned); the arm is one line and reads as described.

S2 — "{} when the request carries no body." Right on the standard transport, for the route seam and the fallback seam, both of which build the request through runHandler: declared JSON that is blank or whitespace is {} (:145); a request whose framing headers declare no body is {} whatever it declares (declaresBody, :127-131, :164-166). Pinned: "an empty or whitespace-only JSON body is {}", "a request with no body is {}, whatever it declares", "CONTROL over the wire: a POST with no body is {}". Against the other producers: the conformance NodeHttpServer starts from body = {} (qa/http-conformance/src/adapter.ts:381), so a no-body request is {} there too; the rest test builder leaves body ABSENT unless a fixture passes one (http-request-test-builder.ts:77-78, :131-139) — a test-layer file that tsup never emits (:6-8), whose header records absence as "the fixture supplied none". The standard transport's own middleware seam hands body: undefined on every request (adapter.ts:1487), but the contract already says so in its own words: the Middleware TSDoc (http-server.ts:144, "req.body is NOT populated") and the fallback TSDoc (:395-398) scope the {} sentence to route and fallback handlers. What the sentence leaves unexplained is the ? on the member next to prose that names {}: the type permits an absence the prose never describes. A residual carried to ③ (Q1), not a contradiction.

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 (:142-149), a form that does not parse (:157-158), and a declared body in a media type the transport does not read (:164-165). I read it as the dev does (acceptance note): MAY, with the standard transport named in S4 as the one that does. The declarative mood of "an implementation that cannot parse the body … throws" can also be read as a norm for every implementation, and under that reading the conformance NodeHttpServer (catch { body = {}; }, :387) is non-compliant. That ambiguity is not a defect in this sentence; it is why the out-of-scope finding is a card (③).

S4 — "The standard transport (@objectstack/plugin-hono-server) does, and answers the request itself with 400 INVALID_REQUEST, replacing whatever the handler wrote;" Right. The body getter records the refusal and throws (adapter.ts:728-734). After the handler settles, if (refusedBodyRead) returns c.json(envelope.body, envelope.status) BEFORE outcome.response ?? capturedResponse is consulted (:880-887 against :893), so a res.json, a res.send and a res.write stream the handler produced are all discarded, and the explicit status argument overrides any res.status() the handler set (hono 4.13.12 context.js #newResponse: a numeric argument wins over the context status). RequestBodyRefusal carries status = 400 and code = 'INVALID_REQUEST' (request-body.ts:86-94), the two conditions declaredEnvelopeForThrow requires (adapter.ts:341-348). Pinned: the query door's own catch answers 500 and the wire shows 400 (steps 2, 3, 4 and the socket case); "a fallback that reads the body is refused like a route". One nuance, non-blocking: a response HEADER the handler set through res.header() lives on the Hono context (c.header writes #preparedHeaders) and rides onto the 400; the body and the status are replaced, a header is not. "Whatever the handler wrote" reads as its answer, which is what the pins measure.

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 (:730), before the throw, so the adapter's answer is decided by the time any catch runs; what survives a catch is the handler's side effects, never its answer. The card's phrase "would write after a 400" was the imprecise form; the TSDoc states the measured one (the dev's Q2). No in-repo consumer catches and carries on: the dispatcher bridge reads req.body unguarded (dispatcher-plugin.ts:303) so the throw escapes to runHandler, and PR #24005's review (residual 8) found no catching domain.

S6 — "A handler that needs the raw bytes (a file upload, a media type not listed above) reads {@link rawBody} and never reads body." Right, and "never reads body" is a real constraint for the case that matters, not advice overstated. For a media type not listed above with a declared body: nothing is read before the handler (:162-166), rawBody() reads c.req.arrayBuffer() from the untouched stream (adapter.ts:741-744), and ANY read of body — inside a try or not — is a recorded refusal that dooms the answer to 400 (S4). Pinned by the three byte-door CONTROL rows (text/plain, application/octet-stream, and a JSON-with-comments file sent as application/json). For the listed media types the sentence is advice: the parse runs before the handler whether or not body is read (:701), and reading body consumes nothing rawBody() needs, because Hono's body cache serves both — text() then arrayBuffer() round-trips the UTF-8 bytes (hono 4.13.12 request.js #cachedBody; the JSON-with-comments pin is exactly this path). So the wording does not overclaim. What is imprecise is the parenthetical's "a file upload": a multipart/form-data upload IS a form that body parses (S1), and on the standard transport its rawBody() is NOT the wire bytes — parseBody() caches formData (utils/body.js → parseFormData → request.formData()), and arrayBuffer() afterwards is new Response(formData).arrayBuffer(), a re-serialisation with a fresh boundary. That shape predates PR #24005 (the old adapter called parseBody() on every non-JSON media type), is unchanged by this diff, and is not pinned either way. A reader who takes "a file upload" to mean multipart is pointed at rawBody() for bytes it does not get. Wording residual: "a raw upload (a PUT of the file's own media type)" names the case the transport's module note names (request-body.ts:49-60, the storage presign). Non-blocking here; the multipart rawBody() shape is escalated in ③.

Type and member check. The hunk is +17/−1 inside the TSDoc block; body?: any (:40) is byte-identical; rawBody's TSDoc (:47-52) is untouched; no other member, no export (check:api-surface sits inside the green Lint & Repo Gates; Type Check · source gates and the TypeScript Type Check aggregate are green). The dev's acceptance note on rawBody's "consume the underlying request stream lazily" is right: JSON and form bodies were parsed before the handler before PR #24005 too, and the media types body does not read stay unread until rawBody(); the old text does not contradict the new one.

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 rawBody() and never reads body" — right (S6; rawBody is a function, so the parentheses are exact). "Unchanged: the type (body?: any), every other member, and every runtime behaviour. This is prose in the shipped .d.ts only." — right against the diff.

PR body. Every pin the measured table names exists under that name in request-body-refusal.test.ts (checked one by one). "JSON is read with text(), which fills the same Hono body cache" is right (request-body.ts:137-141; hono's json() is #cachedBody("text") then JSON.parse).

② Semver level

patch for @objectstack/spec is right. The change is prose in the published .d.ts: files[] carries dist, exports['./contracts'] resolves to dist/contracts/index.d.ts and index.d.mts, and the build's dts pass runs unless OS_SKIP_DTS is set (package.json build, tsup.config.ts:324). Nothing exported is added, removed or narrowed; no authorable key; no ADR-0087 id — so Clause-②: no, present on the changeset and on the PR body, is right and no disposition marker is owed. I did not rebuild (read-only); the dev's reported dist grep — the new sentence in both .d.ts files, the old sentence in neither, the untouched rawBody text as positive control — is consistent with that configuration. Check Changeset is green on this head; check:api-surface, check:docs and check-changeset-fixed sit inside the green Lint & Repo Gates.

③ Boundary flags

  1. Deviation 1, the trailer amend. Accepted. The head commit carries the model-free pair and the session trailer; the amended tip was unpublished (an empty git diff against the gated tree, per the report), and the tree is what this record judges.
  2. Deviation 2, the worktree attached to an existing branch. Accepted. The merge base is e246f0611, the base the claim named.
  3. Deviation 3, local narrowing. Accepted. The four exit-3 families are prerequisite failures of a shallow clone or an unbuilt sibling, none reads the two changed files, and CI runs them; spec pnpm test was declared to Test Core, which is read in item 8.
  4. Deviation 4, no subscribe_pr_activity. Not a review matter.
  5. Open question 1 ({} versus an absent body). A is right. The card forbids a type change; the contract's own Middleware and fallback TSDoc already scope the {} sentence to the seams that populate body; the one producer that leaves it absent is a test fixture outside dist whose header says why; no real handler was shown to break on undefined. B is a type change for a card of its own, owed only on such a measurement. Residual stays as stated in S2.
  6. Open question 2 (the handler consequence). A is right; the sentence states the measured shape (S4, S5) and invents no API.
  7. Out-of-scope finding, the conformance adapter. A card, yes. @objectstack/http-conformance describes itself as "a zero-dependency node:http REFERENCE implementation of IHttpServer plus a cross-adapter suite" — the artefact a third-party adapter author copies, and the suite the contract itself names as what "locks cross-adapter behavior" (http-server.ts, the write member). Measured on main, its adapter diverges from the new contract on three counts, not the one the dev named: a malformed declared-JSON body becomes {} (:387); a multipart/form-data body with content becomes {} — not parsed, not refused (isEagerlyParsed, :102-107, :381), against S1; a text/* body becomes a string (:391), a value the contract never names. PR fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005's review accepted the first "for the private harness" while the contract said nothing; now the contract says what body is and the reference implementation does not deliver it (Prime Directive chore: version packages #10 on the reference, not on a product server). No conformance test drives NodeHttpServer with a malformed body — the only refusal suite (hono-body-refusal.conformance.test.ts) drives the two Hono doors — so the suite cannot pin the refusal for a third-party adapter either. Lane: the one that owned Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914 and fix(plugin-hono-server,hono,cloud-connection)!: refuse an unparseable request body with 400 INVALID_REQUEST instead of reading it as {} #24005, domain:cli · area:api, grade for the triage seat (a validation instrument, no runtime path; p3 reads right). ⛔ Not this PR.
  8. Escalated to the seat, from S6: multipart rawBody() on the standard transport. Derived from hono 4.13.12's cache code, not run: after the eager parseBody(), rawBody() returns a re-serialised form whose boundary does not match the request's Content-Type header, while the untouched rawBody TSDoc promises "binary uploads (file streams, multipart parts)". Pre-existing, unchanged by this diff, not pinned in either direction. No in-repo handler reads multipart wire bytes (the storage raw doors take the file's own media type), so this is a candidate card for the transport lane only after a measurement, not a finding against this PR.
  9. Wording residual, S6's "a file upload". Non-blocking; "a raw upload (a PUT of the file's own media type)" would close it in a follow-up docs edit, or alongside item 7's card.
  10. Check-runs on 2a55ea381bfeda6b6b3cba3c7520c251a6222aa0, read at 2026-10-16T23:18Z. 40 runs, none failed. Success: Build Core, Lint & Repo Gates, TypeScript Type Check (aggregate) with Type Check · source gates, · workspace, · consumer gates and · debt ledger, Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Spec property liveness, every Lint · sub-job, the four claim guards, Auto Label, the Dogfood Regression Gate aggregate, filter. Skipped by design: the dogfood matrix shards, Dogfood Verify CLI, Temporal Conformance, Build Docs, Console Pin Gate, Packed-tarball smoke. Test Core (1/6), (2/6), (3/6), (5/6) and (6/6) are green; Test Core (4/6) was still in progress at that reading, so this record has NOT read that one shard; the diff is TSDoc plus a changeset, Build Core (the spec build the tests consume) is green, and the seat reads the shard before the queue. The Docs Drift Check comment is advisory and lists no page.

Implemented-by: claude/issue-24043-http-request-body-tsdoc
Reviewed-by: session_01HjR8RUwTPQ3TgbfKihmxp9

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #24068 @ 2a55ea381b (#24043) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T23:20Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6226147853 on #24043).

  • Shape: Draft, base main. The first line is Fixes #24043, the body's only closing keyword, then Clause-②: no, matching claim 6225842206. 2 files, +29/−1: packages/spec/src/contracts/http-server.ts (the IHttpRequest.body TSDoc only) and one patch changeset for @objectstack/spec. Not governed (check-governed-merges.mjs --pr 24068). Assignee os-elon-musk.
  • What it lands: IHttpRequest.body's TSDoc now says:
    • the body is parsed by its declared content type (JSON for application/json, form fields for multipart/form-data and application/x-www-form-urlencoded), and is {} when the request carries no body;
    • reading it may refuse. That is stated as permission for an implementation; the standard transport (@objectstack/plugin-hono-server) is named as the one that does, and it answers 400 INVALID_REQUEST itself, replacing whatever the handler wrote, so a handler must not catch the refusal and carry on;
    • a handler that needs raw bytes reads rawBody().
      The type (body?: any) and every other member are unchanged.
  • Contract review: PASS 6226498420 on this head (CONTRACT_REVIEW_TIER).
    • The three media types are exactly the three readRequestBody parses (request-body.ts about :136, :153-:156).
    • {} for no body holds on the route and fallback seams and on the conformance adapter. The middleware seam and the rest test builder leave it absent, which the contract's own Middleware TSDoc already scopes.
    • The 400-replaces-the-handler's-answer sentence matches adapter.ts (about :880-:887), and "already refused" is exact because the refusal is recorded at the read.
    • "never reads body" is a real constraint for the unlisted media types and harmless advice for the listed ones, so it does not overclaim.
    • patch with Clause-②: no matches what ships (dist/contracts/index.d.ts / .d.mts).
  • The report's open questions, the seat's calls (both confirmed by the review):
    • (1) "{} when the request carries no body" stays as written, with the member optional: A. The card forbids a type change, the contract already scopes undefined to the middleware seam, and no handler was shown to break.
    • (2) The handler sentence as written: A. It states the measured behaviour; the card's "would write after a 400" was the less precise form.
  • Deviations: the amended trailer was on an unpublished tip (tree unchanged); the worktree was on the same base; four exit-3 families were unmeasured locally and are covered by CI. All accepted.
  • CI on this head: 41 check-runs, 35 success, 6 skipped by their filters, 0 failed, 0 in progress. mergeable: clean.

At landing, this seat files one card, bug · area:api · p3 for triage, lane domain:cli (the lane of #23914 / #24005). The review found that the conformance adapter (packages/qa/http-conformance/src/adapter.ts), the reference implementation the contract's cross-adapter suite runs, now diverges from this TSDoc on three counts:

  • a malformed declared-JSON body becomes {};
  • a multipart/form-data body with content becomes {}, neither parsed nor refused;
  • a text/* body becomes a string, a value the contract never names.

The card will also carry the review's wording residual: "a file upload" in the raw-bytes sentence invites the multipart reading, and "a raw upload (a PUT of the file's own media type)" is the case the transport's module note names.

Noted, not filed (unmeasured): the review read, but did not run, hono 4.13.12's body cache. After the eager parseBody(), rawBody() on a multipart request would return a re-serialised form with a fresh boundary, while rawBody's untouched TSDoc says "multipart parts". The behaviour is pre-existing, nothing in the repo reads multipart wire bytes, and neither behaviour is pinned. Dedupe words: multipart rawBody re-serialised boundary; hono cachedBody rawBody after parseBody.

The PR goes to the merge queue in this act. The cloud dev session is archived at this ACCEPT.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

2 participants