Skip to content

perf(body-limit): stream enforcement instead of pre-buffering#1500

Merged
pi0 merged 14 commits into
mainfrom
perf/body-limit
Jul 22, 2026
Merged

perf(body-limit): stream enforcement instead of pre-buffering#1500
pi0 merged 14 commits into
mainfrom
perf/body-limit

Conversation

@pi0x

@pi0x pi0x commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Closes #1498.

What

bodyLimit() / assertBodySize() verified request body size by draining a cloned request stream before the handler ran — adding latency, holding up to limit bytes in memory per in-flight request, and blocking streaming handlers. This switches to streaming enforcement using srvx's pull-based limiter (now publicly exported at srvx/body-limit), keeping the exact same byte-accurate guarantee with no pre-buffering.

How

  • assertBodySize wraps event.req.body with limitBodyStream(...) and swaps in a rebuilt Request, so bytes are counted as the handler reads them — no clone(), no pre-buffer, streaming handlers start immediately.
    • srvx's limitRequestBody can't be used directly: its internal new Request(request, ...) throws on srvx's Node ServerRequest (Cannot read private member #state). Instead we wrap req.body with limitBodyStream and rebuild a native Request, re-attaching srvx's runtime augmentation (runtime / waitUntil / ip / context) — mirroring createSubRequest in proxy.ts. Verified event.runtime and getRequestIP keep working after the swap.
  • Preserved & still synchronous: fail-fast 413 for an honest oversized Content-Length, and 400 for Content-Length + Transfer-Encoding (RFC 7230 smuggling).
  • assertBodySize is now synchronous (void) — no draining left to await.
  • Error mapping: a deferred overflow surfaces as srvx's ERR_BODY_TOO_LARGE. It's mapped to a handled 413 in toResponse (so it isn't logged as an unhandled server error), and readBody's formData() catch re-throws it instead of masking it as 400.

Trade-offs (documented in JSDoc)

  1. Overflow on a chunked / unknown-length body surfaces mid-read rather than as a pre-handler 413 (the fail-fast 413 is kept wherever an honest Content-Length is present).
  2. Enforcement is tied to consumption — a body the handler never reads is not counted (benign; bounded by transport backpressure).

Notes / things to review

  • srvx bumped to ^0.12.2 (adds the srvx/body-limit entry point).
  • H3Core bundle guardrail nudged 7550 → 7600 bytes (3000 → 3050 gzip): the ERR_BODY_TOO_LARGE check sits in the toResponse hot path (~13B). Flagging for a conscious ok.
  • The oversized-formData() integration test is node-target only: an in-process FormData body is a lazy undici stream that emits an unhandled "already closed" rejection when the multipart parse is aborted mid-flight — an artifact of the in-memory source, not of real socket-backed requests (verified clean over a real server).

Tests

New test/body-limit.test.ts (matrix) + rewritten test/unit/body-limit.test.ts cover: fail-fast 413, chunked overflow surfacing mid-stream, lying-small Content-Length, CL+TE 400, formData413 mapping, and an unread body not being counted. Full suite: 1592 passed, lint + typecheck clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enforced request body limits while streaming/consuming the body, with oversized payloads consistently returning 413 Request Entity Too Large.
    • Preserved existing limit errors as-is (instead of converting them to generic parsing/invalid-body errors) across form, JSON, validation, and JSON-RPC flows.
    • Added fail-fast handling for conflicting Content-Length and Transfer-Encoding scenarios to avoid request smuggling.
  • Documentation
    • Updated body-limit and body-size enforcement docs to precisely describe error timing and streaming behavior.
  • Tests
    • Added/expanded matrix and unit tests to cover buffered, streaming, chunked, multipart, and edge header cases.

`bodyLimit()` / `assertBodySize()` verified body size by draining a cloned
request stream before the handler ran, adding latency and holding up to
`limit` bytes in memory per request. Replace this with srvx's pull-based
stream limiter (`srvx/body-limit`, now publicly exported):

- `assertBodySize` wraps `event.req.body` with `limitBodyStream` and swaps in
  a rebuilt `Request`, so bytes are counted as the handler reads them — no
  clone, no pre-buffer, and streaming handlers start immediately. The rebuilt
  `Request` re-attaches srvx's runtime augmentation (`runtime`/`waitUntil`/
  `ip`/`context`), mirroring `createSubRequest` in `proxy.ts`.
- The fail-fast `413` (honest oversized `Content-Length`) and the `400`
  Content-Length + Transfer-Encoding smuggling check are preserved and stay
  synchronous.
- `assertBodySize` is now synchronous (no draining).
- A deferred overflow surfaces as srvx's `ERR_BODY_TOO_LARGE`; it is mapped to
  a handled `413` in `toResponse`, and `readBody`'s `formData()` catch
  re-throws it instead of masking it as `400`.

Trade-offs (documented): overflow on a chunked / unknown-length body surfaces
mid-read rather than as a pre-handler `413`, and a body the handler never
reads is not counted.

Bumps srvx to ^0.12.2 (adds the `srvx/body-limit` entry). The `ERR_BODY_TOO_LARGE`
check in the response hot path nudges the H3Core bundle guardrail by ~13B.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@pi0x
pi0x requested a review from pi0 as a code owner July 22, 2026 11:59
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

assertBodySize now enforces limits while request streams are consumed. Header-based rejection and smuggling checks remain, middleware applies enforcement synchronously, parsing preserves body-limit errors, and tests and documentation cover the updated behavior.

Changes

Streaming body limits

Layer / File(s) Summary
Stream-wrapped body enforcement
src/utils/internal/body.ts, src/utils/body.ts
limitBody counts bytes during stream consumption and raises 413 on overflow; assertBodySize retains header checks and rebuilds the request with the limited stream.
Overflow error propagation
src/utils/body.ts, src/utils/internal/validate.ts, src/utils/json-rpc.ts
HTTP body-limit errors remain intact through form-data, validated JSON, and JSON-RPC parsing.
Middleware and response integration
src/utils/middleware.ts, docs/2.utils/1.request.md, docs/2.utils/9.more.md
bodyLimit calls synchronous enforcement, and documentation describes streaming, header, and consumption behavior.
Body-limit behavioral coverage
test/body-limit.test.ts, test/unit/body-limit.test.ts
Integration and unit tests cover buffered, streamed, chunked, lying-header, unread-body, parsing, multipart, and smuggling cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • h3js/srvx#279 — Requests a reusable pull-based streaming body-limit approach implemented here through limitBody.

Possibly related PRs

  • h3js/h3#1164 — Intersects with the updated readBody form-data error handling.
  • h3js/h3#1320 — Also addresses cancelling or aborting the body stream when size limits are exceeded.

Suggested reviewers: pi0

Poem

I’m a rabbit guarding the stream,
Counting each byte in a silvery gleam.
Too-large parcels get 413,
Smuggling tricks get stopped promptly.
I twitch my nose: the body flows free!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the core change from pre-buffering to streaming body-limit enforcement.
Linked Issues check ✅ Passed The implementation matches #1498 by preserving CL/TE checks, enforcing limits during consumption, handling unread bodies, and adding tests/docs.
Out of Scope Changes check ✅ Passed The docs, helpers, and error-handling updates all support the body-limit streaming change and are within scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/body-limit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… paths

Extract `isBodyLimitError` to `utils/internal/body.ts` and reuse it where a
body-read failure is wrapped into a friendlier error, so a deferred
`ERR_BODY_TOO_LARGE` surfaces as `413` instead of being masked:

- `validatedRequest` (`defineValidatedHandler`) JSON path: was masking the
  overflow as `400 Invalid JSON body`.
- `defineJsonRpcHandler`: was masking it as a JSON-RPC parse error (HTTP 200).

Adds regression tests for both paths.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@pi0x

pi0x commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: independent validation pass

Ran an adversarial review of the change. Results:

  • ✅ lint + typecheck + full suite (1596 passed, no unhandled rejections)
  • ✅ byte-accurate guarantee intact (lying-small Content-Length still caught mid-stream; non-numeric/negative CL safely falls through to the stream limiter — no bypass)
  • event.url, getRequestIP, event.runtime/waitUntil all verified working after the event.req swap

Fixed a real defect it surfaced (2nd commit): the deferred ERR_BODY_TOO_LARGE was being masked as 400/parse-error in two other consumption paths besides readBody's formData():

  • defineValidatedHandler JSON path (internal/validate.ts) → was 400 Invalid JSON body
  • defineJsonRpcHandler (json-rpc.ts) → was a JSON-RPC parse-error envelope (HTTP 200)

Both now re-throw the overflow so it maps to 413, via a shared isBodyLimitError helper. Regression tests added for both.

One item left for a maintainer call: the rebuilt Request re-attaches runtime/waitUntil/ip/context but not tls (srvx mtls plugin). No h3 util reads event.req.tls, so a handler reading it after bodyLimit/assertBodySize would see undefined. Niche — happy to add limited.tls = req.tls if you'd like it covered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/utils/body.ts (1)

258-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export and reuse isBodyLimitError instead of duplicating the ERR_BODY_TOO_LARGE check. Both sites independently test the same error shape against the same magic string with no shared source of truth.

  • src/utils/body.ts#L258-L260: export isBodyLimitError (drop the file-local visibility) so it can be imported elsewhere.
  • src/response.ts#L108-L119: import isBodyLimitError from src/utils/body.ts and replace the inline (val as { code?: string }).code !== "ERR_BODY_TOO_LARGE" check with !isBodyLimitError(val).
♻️ Proposed refactor
--- a/src/utils/body.ts
-/** Whether an error was thrown by the srvx body-size limiter. */
-function isBodyLimitError(error: unknown): boolean {
+/** Whether an error was thrown by the srvx body-size limiter. */
+export function isBodyLimitError(error: unknown): boolean {
   return (error as { code?: string } | undefined)?.code === "ERR_BODY_TOO_LARGE";
 }
--- a/src/response.ts
+import { isBodyLimitError } from "./utils/body.ts";
...
-    if (!isHTTPError && (val as { code?: string }).code !== "ERR_BODY_TOO_LARGE") {
+    if (!isHTTPError && !isBodyLimitError(val)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/body.ts` around lines 258 - 260, Export isBodyLimitError from
src/utils/body.ts so it is reusable, then import it in src/response.ts and
replace the inline ERR_BODY_TOO_LARGE check with !isBodyLimitError(val). Update
both affected sites: src/utils/body.ts lines 258-260 and src/response.ts lines
108-119, preserving the existing response behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/utils/body.ts`:
- Around line 258-260: Export isBodyLimitError from src/utils/body.ts so it is
reusable, then import it in src/response.ts and replace the inline
ERR_BODY_TOO_LARGE check with !isBodyLimitError(val). Update both affected
sites: src/utils/body.ts lines 258-260 and src/response.ts lines 108-119,
preserving the existing response behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b951d055-a108-4fc1-86a2-b0fc1dd2e6b0

📥 Commits

Reviewing files that changed from the base of the PR and between b3c3eff and 6345f6d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • docs/2.utils/1.request.md
  • docs/2.utils/9.more.md
  • package.json
  • src/response.ts
  • src/utils/body.ts
  • src/utils/middleware.ts
  • test/bench/bundle.test.ts
  • test/body-limit.test.ts
  • test/unit/body-limit.test.ts

…ndary

Instead of letting srvx's raw `ERR_BODY_TOO_LARGE` propagate and detecting it
in every consumer (`response.ts`, `readBody`, validated handlers, JSON-RPC), a
single internal `limitBody()` helper wraps srvx's `limitBodyStream` and aborts
with a proper `413` `HTTPError`. Every body consumer now only ever sees an
`HTTPError`, so the body readers that wrap failures just re-throw a pre-existing
`HTTPError` (general `HTTPError.isError` guard) rather than sniffing a
srvx-specific error code.

- `ERR_BODY_TOO_LARGE` is referenced in exactly one place now.
- `response.ts` needs no special case (reverted); the `413` flows as a normal
  handled error, so the H3Core bundle guardrail is restored to its old value.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use an options object for the new helper.

Expose the limit as { limit } and update its caller to avoid positional-argument drift.

As per coding guidelines: “Use options object as second parameter for multi-argument functions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/internal/body.ts` around lines 42 - 45, Update the limitBody helper
to accept an options object containing the limit property instead of a
positional numeric argument, and update every caller to pass { limit }
accordingly. Preserve the existing body-limiting behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/utils/internal/body.ts`:
- Around line 42-45: Update the limitBody helper to accept an options object
containing the limit property instead of a positional numeric argument, and
update every caller to pass { limit } accordingly. Preserve the existing
body-limiting behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6a1b397-c5f6-43f6-a8b2-47193149a042

📥 Commits

Reviewing files that changed from the base of the PR and between 6c288d4 and 9b508d1.

📒 Files selected for processing (5)
  • src/utils/body.ts
  • src/utils/internal/body.ts
  • src/utils/internal/validate.ts
  • src/utils/json-rpc.ts
  • test/unit/body-limit.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/utils/json-rpc.ts
  • src/utils/body.ts
  • test/unit/body-limit.test.ts

`limitBody` wrapped srvx's `limitBodyStream` in a second ReadableStream just to
convert its error — two wrappers and two reader hops per chunk. Replace it with
a single pull-based stream that counts bytes and aborts with a `413` HTTPError
directly (the mirror approach pi0 listed on #1498). One wrapper, no error
translation, and no `srvx/body-limit` dependency — reverts the srvx bump.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@pi0x

pi0x commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Update: inlined the limiter, dropped the srvx/body-limit dependency

Two rounds of cleanup on the error handling:

  1. Instead of letting srvx's raw ERR_BODY_TOO_LARGE propagate and sniffing for it in response.ts / readBody / validated handlers / JSON-RPC, the limiter now aborts the body stream with a real 413 HTTPError at the source. Consumers just re-throw a pre-existing HTTPError (general HTTPError.isError guard) rather than masking it — no error-code string scattered around, and response.ts needs no special case.

  2. That first pass wrapped srvx/body-limit's limitBodyStream in a second stream purely to translate the error — a wasteful double-wrap. Replaced with a single ~15-line pull-based limiter that counts + throws the HTTPError directly. This is the "mirror the helper in h3" option you listed in the issue comment, and it lets us keep the HTTPError shape consistent.

Net effect vs. the earlier commits: no srvx version bump (reverted — srvx/body-limit is no longer imported), and the H3Core bundle guardrail is back to its original 7550/3000 (the limiter now lives outside the core toResponse path).

Still green: 1596 passed, lint + typecheck clean, no unhandled rejections.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)

40-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use an options object for the limit configuration.

Change this to limitBody(body, { limit }) and update its caller so the internal API remains extensible.

As per coding guidelines, use an options object as the second parameter for multi-argument functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/internal/body.ts` around lines 40 - 43, Update limitBody to accept
a second-parameter options object containing limit instead of a numeric limit,
and adjust its caller to invoke it with { limit }. Preserve the existing
body-limiting behavior while keeping the internal API extensible.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/internal/body.ts`:
- Around line 46-48: Update the ReadableStream created by limitBody so it does
not eagerly read from the upstream reader before downstream demand, using a zero
highWaterMark queuing strategy or equivalent pull gating. Preserve the existing
reader.read and cancellation behavior.

---

Nitpick comments:
In `@src/utils/internal/body.ts`:
- Around line 40-43: Update limitBody to accept a second-parameter options
object containing limit instead of a numeric limit, and adjust its caller to
invoke it with { limit }. Preserve the existing body-limiting behavior while
keeping the internal API extensible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecea2651-af8f-474b-bd1a-7effc732eee6

📥 Commits

Reviewing files that changed from the base of the PR and between 9b508d1 and 9c89a3e.

📒 Files selected for processing (1)
  • src/utils/internal/body.ts

Comment thread src/utils/internal/body.ts Outdated
pi0 and others added 2 commits July 22, 2026 14:27
Replace assertBodySize's `new Request` rebuild + manual srvx-augmentation
copy with a Proxy over the original ServerRequest. Body reads route through
a single lazy limitBody stream (via a Response for its parsers); everything
else — headers, url, runtime, waitUntil, ip, context — falls through
Reflect.get, so future srvx props can't silently drop. Also removes the
duplex @ts-expect-error and defers stream wrapping until first read.

Mirrors the existing proxy in validatedRequest.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)

31-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use options objects for both limiting helpers.

  • src/utils/internal/body.ts#L31-L70: Change limitBody(body, limit) to accept { limit }.
  • src/utils/internal/body.ts#L95-L95: Change limitRequestBody(req, limit) to accept { limit }, then update call sites.

As per coding guidelines, “Use options object as second parameter for multi-argument functions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/internal/body.ts` around lines 31 - 70, Update limitBody to accept
an options object containing limit instead of a positional second argument, and
update limitRequestBody similarly to accept { limit }. Adjust every call site of
both helpers to pass the new options-object form while preserving existing
request-body limiting behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/internal/body.ts`:
- Around line 112-115: Update the proxy get logic around bodyReadMethods so the
clone() method returns a request whose body-reading methods use limitedBody(),
preventing cloned requests from accessing the original unlimited stream.
Preserve existing direct body-read handling and add a regression covering
text/json reads through event.req.clone().

---

Nitpick comments:
In `@src/utils/internal/body.ts`:
- Around line 31-70: Update limitBody to accept an options object containing
limit instead of a positional second argument, and update limitRequestBody
similarly to accept { limit }. Adjust every call site of both helpers to pass
the new options-object form while preserving existing request-body limiting
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c2d3ebe-2aff-4227-865c-15bfe9c79895

📥 Commits

Reviewing files that changed from the base of the PR and between 9c89a3e and 8172a87.

📒 Files selected for processing (3)
  • docs/2.utils/1.request.md
  • src/utils/body.ts
  • src/utils/internal/body.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/body.ts
  • docs/2.utils/1.request.md

Comment thread src/utils/internal/body.ts Outdated
Comment on lines +112 to +115
if (bodyReadMethods.has(prop as any)) {
return () => (limitedBody() as any)[prop]();
}
return Reflect.get(target, prop);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect relevant sections.
ast-grep outline src/utils/internal/body.ts --view expanded || true
echo '---'
wc -l src/utils/internal/body.ts
echo '---'
sed -n '1,220p' src/utils/internal/body.ts

Repository: h3js/h3

Length of output: 4679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/utils/internal/body.ts --view expanded || true
echo '---'
wc -l src/utils/internal/body.ts
echo '---'
sed -n '1,220p' src/utils/internal/body.ts

Repository: h3js/h3

Length of output: 4679


Limit cloned requests too. clone() falls through to Reflect.get, so event.req.clone().text() / .json() can read the original unlimited body stream. Wrap cloned requests as well and add a regression for cloned-body reads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/internal/body.ts` around lines 112 - 115, Update the proxy get
logic around bodyReadMethods so the clone() method returns a request whose
body-reading methods use limitedBody(), preventing cloned requests from
accessing the original unlimited stream. Preserve existing direct body-read
handling and add a regression covering text/json reads through
event.req.clone().

pi0 and others added 4 commits July 22, 2026 14:42
srvx 0.12.3 (h3js/srvx#281) makes `limitRequestBody` preserve ServerRequest
(proxy-wrap, generic `<T extends Request>`, handles clone()), so h3 no longer
needs its own inline proxy + stream limiter. Delete both and delegate the wrap
to `srvx/body-limit`; keep the synchronous fail-fast checks (413 honest oversized
Content-Length, 400 CL+TE smuggling) in `assertBodySize`.

srvx throws its canonical `ERR_BODY_TOO_LARGE` (status 413) rather than an
HTTPError, so map it back to a handled 413 at every consumption boundary:
`isBodyLimitError` re-throws it (readBody/formData, validated json, json-rpc)
instead of masking as 400, and `toResponse` renders a handled 413 instead of an
unhandled 500 (inlined there to keep the helper out of the core bundle). A
handler reading `event.req` directly now sees the ERR_BODY_TOO_LARGE error.

Bundle guardrails nudged for the toResponse check: H3Core 7600->7650 (gzip
3000->3050), defineHandler 6700->6750 (2700->2750), H3 gzip 6950->7000.

Full suite: 1598 passed, lint + typecheck clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsrvx@​0.12.4681009696100
Addedh3-nightly@​2.0.0-20260722-142733-4a32c1b77100879680

View full report

pi0 and others added 4 commits July 22, 2026 17:00
srvx 0.12.4 (h3js/srvx#282) lets `limitRequestBody` take a `createError`
factory, so h3 hands it its own `HTTPError` instead of srvx's
`ERR_BODY_TOO_LARGE`. The overflow is now an `HTTPError` at the point it's
thrown, so every consumer treats it correctly with no new code:

- Revert the core error path: `toResponse` no longer special-cases the srvx
  error code (no change to `response.ts`), and the `isBodyLimitError` helper
  is gone from `error.ts`. No changes to the hot path, no bundle-size bump.
- The body-reader catches (`readBody`/formData, validated json, json-rpc)
  re-throw it via their existing `HTTPError.isError` guard.
- A raw `event.req.text()` overflow throws an `HTTPError` again (guarantee
  restored); unit test reverted accordingly.

Net vs main: delete the inline limiter and route `assertBodySize` through
srvx's proxy — no core changes, no scattered error mapping.

Full suite: 1598 passed, lint + typecheck clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@pi0
pi0 merged commit cad67c8 into main Jul 22, 2026
8 of 9 checks passed
@pi0
pi0 deleted the perf/body-limit branch July 22, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bodyLimit/assertBodySize: stream enforcement instead of pre-buffering the body

2 participants