Repository navigation
fix(plugin-hono-server, hono)!: a request body over the server's limit is answered 413 PAYLOAD_TOO_LARGE and is not read past the limit - #33172
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
…own bound Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
…envelope audit Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
…ARGE Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
📓 Docs Drift CheckThis PR changes 4 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 176 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 1a37cc35bf5dda750c101dc07455e048617582b3 && git checkout 1a37cc35bf5dda750c101dc07455e048617582b3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88355328f0399bec54337791a6656b8b385d3644 bb70828123790b3d9fdc3f58bdc1f8bf19389c2b && git checkout -B drift-repro 88355328f0399bec54337791a6656b8b385d3644 && git merge --no-ff bb70828123790b3d9fdc3f58bdc1f8bf19389c2b
node scripts/docs-audit/affected-docs.mjs --json 88355328f0399bec54337791a6656b8b385d3644
|
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 Contract-review-claim: session_01L9qibwMmGJKaQ52CgHqNu2 · head 30d0bed · 2026-11-13T23:50Z |
…ile form is bounded by its own limit, not the request-body limit A route refused req.body for size that then reads rawBody() takes the read over under its own bound. The upload-link door reads its several-file form that way when the transport refuses it, judges the bytes against its own form limit, and parses the form itself. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
… its breaking note Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 Contract-review-claim: session_01L9qibwMmGJKaQ52CgHqNu2 · head e85a506 · 2026-11-14T01:06Z |
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 Contract reviewServed-tier: The rest of the diff (the three test files and Check-runs on this head, all completed: 56 success, 13 skipped, 0 failed, 0 pending. Among them ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Ruling arms, in the code:
Findings:
S1 and S2 each earn a delta review of their fix. Neither breaks a ruling arm. Implemented-by: VERDICT: PASS |
…used past the limit releases its source A second rawBody() call answers the same bytes as the first. A body read up to the limit and refused, that no byte door went on to read whole, has its source cancelled at the end of the request. The changeset names @objectstack/spec, whose error-code ledger gains a row, and that row's comment names what createHonoApp imports. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 Contract-review-claim: session_01L9qibwMmGJKaQ52CgHqNu2 · head bb70828 · 2026-11-14T01:58Z |
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 Contract reviewServed-tier: Delta review of one commit on top of Check-runs on this head, all completed: 58 success, 13 skipped, 0 failed, 0 pending. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Open from the earlier record, unchanged by the delta: nits N3, N4 and N5. Each is an accepted deviation or a follow-up. No must-fix and no should-fix is open on this head. Implemented-by: VERDICT: PASS |
|
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2 ACCEPT · 2026-11-14T02:30Z · PR #33172 · head
|
Requested by Tesla · project thread
Clause-②: yes (narrowing) — a JSON, form or other body over 10 MiB that both HTTP doors read today is answered
413 PAYLOAD_TOO_LARGE, unread past the limit, unless an operator raises the limit. The published surface also widens:HonoPluginOptions.maxRequestBodyBytes,createHonoApp'smaxRequestBodyBytesoption,HonoHttpServer.setMaxRequestBodyBytes()/getMaxRequestBodyBytes(), theOS_HTTP_MAX_BODY_BYTESenvironment variable, and thebody-limitexports of@objectstack/plugin-hono-server.Metadata-protocol: none
Skills-docs: follow-up owed — content/docs/api/error-catalog.mdx (the
VALIDATION_ERRORcallout) and content/docs/protocol/kernel/error-handling.mdx (the 413 row) list the causes of413 PAYLOAD_TOO_LARGEand do not yet name a request body over the server's limit, normaxRequestBodyBytes/OS_HTTP_MAX_BODY_BYTES; no file underskills/**teaches a request-body limit.Refs #32004
维护者速读
plugin-hono-server和createHonoApp)现在共用同一个请求体上限,默认 10 MiB。超过上限的请求回 413PAYLOAD_TOO_LARGE:声明了长度的不读,没声明长度的读到上限就停。运维可以用maxRequestBodyBytes或OS_HTTP_MAX_BODY_BYTES调整。max_upload_mb判断;上传链接的多文件表单按它自己的表单上限判断。packages/spec只在错误码台账里补了一行来源记录。回滚就是还原本 PR。packages/plugins/plugin-hono-server/src/body-limit.ts(守卫本体,重点看guardRequest和installRequestBodyLimit的 413 覆盖)、request-body.ts里readRequestBody的新开头、adapter.ts里rawBody改为读原始请求体、storage-routes.ts里uploadLinkFormBody。pnpm --filter @objectstack/hono exec vitest run src/hono-body-refusal.test.ts,预期 63 个用例全部通过。What changes
The rule. Both Hono doors hold one request-body limit, 10 MiB unless an operator sets another. A request whose declared
Content-Lengthis over the limit is not read. A body with no declared length is counted while it is read, and reading stops once it passes the limit. Either way, the request is answered413with the registeredPAYLOAD_TOO_LARGEcode, whatever the reader answered. Exactly the limit is read as before. A route that never reads the body reads nothing.plugin-hono-server/src/body-limit.ts(new): the guard, installed as the first middleware on each door's app. It wraps the request's body, so every reader goes through it: the doors' own JSON and form pre-reads, raw-app routes that readc.req, and requests forwarded whole to the auth service or a domain. It also holds the option resolution, theRequestBodyTooLargerefusal and the 413 envelope.413is not inHttpStatusErrorCodeMap, so both doors render this envelope themselves rather than throughdeclaredEnvelopeForThrow.request-body.ts: a declared length over the limit makesreq.bodyrefuse with 413 on read, on the same read-time path as the existing 400 refusals. A JSON or form body with no declared length is read up to the limit and kept. If it passes the limit and no byte door goes on to read it whole, its source is cancelled at the end of the request, as a read refused through the guard is.adapter.ts:rawBody()reads the original body, so a byte door keeps its own bound. It is repeatable: a second call answers the same bytes as the first.setMaxRequestBodyBytes/getMaxRequestBodyByteslet a host change the limit.req.bodyfor size and then readsrawBody()takes the read over under its own bound, so that refusal no longer stands as the answer. Every other refusal still does.hono-plugin.ts:HonoPluginOptions.maxRequestBodyBytes.adapters/hono/src/index.ts:createHonoAppinstalls the same guard first and takesmaxRequestBodyBytes.readDispatchBodyrefuses an over-limit body as too large.thrownJsonrenders the 413.maxRequestBodyBytes(bytes) on the plugin, the adapter andcreateHonoApp; elseOS_HTTP_MAX_BODY_BYTES; else 10 MiB. A value that is not a positive whole number of bytes throws where it is configured. The retired stack keyserver.bodyLimitstays retired; nopackages/speckey is added.service-storage/src/storage-routes.ts: the upload link's several-file form is bounded by that door's own form limit (each file withinmax_upload_mb, times the files the link takes, plus 64 KiB of framing), not by the request-body limit. When the transport refusesreq.bodyfor size, the door reads the form throughrawBody(), judges the bytes against its own limit and parses the form itself. A declared length over the door's limit is still refused before anything is read. The one-file body on the same door is unchanged.packages/spec/src/api/error-code-ledger.zod.ts: one provenance row,@objectstack/plugin-hono-server→PAYLOAD_TOO_LARGE, already a registered code (check:error-code-provenance). Nothing else inpackages/spec.scripts/check-route-envelope.mjs:body-limit.tsdeclared as a conformant response module.Tests
adapters/hono/src/hono-body-refusal.test.ts, describe "a request body over the transport limit is answered 413 and not read past the limit, on both doors". The client body is a stream that counts what the server pulled. Twenty cases:req.bodyroute and a self-hosted raw-app route. With a declared length over the limit each answers 413 with 0 bytes pulled. With no declared length each answers 413 with at most the limit plus two chunks pulled.rawBody()receives a body over the transport limit whole, declared or not, as bytes or as a JSON file (4 cases).plugin-hono-server/src/body-limit.test.ts:req.bodyfor size takes the read over throughrawBody(), and its own answer stands, declared and not.rawBody()called twice answers the same bytes, declared and not.qa/dogfood/test/request-body-limit-upload-doors.dogfood.test.ts, over a real boot under the default limit:max_upload_mb= 2 and a multi-file field:Verification
Suites, ablation and gates
bd827d5d's source (the next commit only rewrites the changeset's declaration lines) (origin/mainmerged at5e250a65):@objectstack/plugin-hono-server: 40 files, 533 tests passed.@objectstack/service-storage: 83 files, 1573 tests passed.@objectstack/hono: 14 files, 258 tests passed.@objectstack/specsrc/api: 63 files, 2060 tests passed.typecheckof both door packages,service-storageanddogfood(with test typecheck): exit 0.@objectstack/speccheck:generated: all 14 artifacts up to date after a spec build.ablation-replace.mjs; each restored with the blob equal to HEAD andgit diff HEADempty):hono-body-refusal.test.ts(every over-limit reader case, the anonymous-route case and the default case), and 2 of 18 red inbody-limit.test.ts(both real-listener 413 cases). The CONTROL, exactly-at-limit, raised-limit and byte-door cases stayed green.rawBody()made to skip the original body: 4 of 63 and 2 of 18 red, every byte-door case.rawBody()take-over removed from the adapter: 2 of 20 red inbody-limit.test.ts, both take-over cases.rawBody()removed: 2 of 9 red in the dogfood file, both "lands whole" cases.dispatch-gates.mjs --ranreconciled 116 derived, 115 run, 1 NOT-MEASURED, 0 unrun.check-plugin-teardown-shape.mjs --self-test, because this is a shallow clone and the fixture is pinned to a commit outside it. CI runs it.check:dts-closureandcheck:dual-build-cjs-loads: exit 0, after a fullpnpm build.e85a5062changes the changeset only (the narrowing arm, the BREAKING banner, the ADR-0087 disposition and theSkills-docs:line). Ate85a5062:check-adr-0087-registration.mjs --base origin/main: one declared-breaking changeset,not-required (no-migration-prescription), exit 0.check-changeset-no-major.mjs --base origin/main --event(this title and body): level axis green,yes (narrowing)withnoneatpatch, exit 0.check-empty-changeset,check:changeset-gate-self-tests: exit 0.bb708281(after the contract review's should-fix items:rawBody()repeatable, a refused held body releases its source,@objectstack/specnamed in the changeset, the ledger row's comment). Atbb708281:@objectstack/plugin-hono-server40 files, 537 tests;@objectstack/hono14 files, 258 tests;@objectstack/specsrc/api63 files, 2060 tests; the dogfood file 9 tests: all passed.rawBody()not held, 1 of 24 red (the declared repeat case).dispatch-gates.mjs --ranreconciled 116 derived, 115 run, 1 NOT-MEASURED (the teardown self-test, shallow clone), 0 unrun.e85a5062: dependency closure built first (pnpm turbo run build --filter='@objectstack/dogfood^...' --concurrency=2, 70 tasks), then under the shared verify lockpnpm --filter @objectstack/dogfood test, no shard and no path: Test Files 463 passed, 3 skipped (466); Tests 3917 passed, 42 skipped (3959); lock VERDICT command-exit 0. No failure.Acceptance notes
IHttpServercontract. The seat chose to keeppackages/specuntouched.rawBody(), which serves the original body, so their bound ismax_upload_mbexactly as before. Next PR (ordered separately):rawBody()takes the door's bound (anIHttpRequest.rawBodycontract widening, with its contract review), so a byte-door body with no declared length is counted againstmax_upload_mbon each of the three doors. The same change updates theIHttpRequestcontract's description of body refusals to include the 413.max_upload_mbunset, a byte door has no bound of its own.max_upload_mbunset that door has no form bound, as before.maxRequestBodyBytesorOS_HTTP_MAX_BODY_BYTES. File uploads through the storage doors are unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibyNMYdfN5kzwSS28Rs