perf(body-limit): stream enforcement instead of pre-buffering#1500
Conversation
`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]>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesStreaming body limits
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
… 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]>
Follow-up: independent validation passRan an adversarial review of the change. Results:
Fixed a real defect it surfaced (2nd commit): the deferred
Both now re-throw the overflow so it maps to One item left for a maintainer call: the rebuilt |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/body.ts (1)
258-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport and reuse
isBodyLimitErrorinstead of duplicating theERR_BODY_TOO_LARGEcheck. 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: exportisBodyLimitError(drop the file-local visibility) so it can be imported elsewhere.src/response.ts#L108-L119: importisBodyLimitErrorfromsrc/utils/body.tsand 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
docs/2.utils/1.request.mddocs/2.utils/9.more.mdpackage.jsonsrc/response.tssrc/utils/body.tssrc/utils/middleware.tstest/bench/bundle.test.tstest/body-limit.test.tstest/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]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)
42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse 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
📒 Files selected for processing (5)
src/utils/body.tssrc/utils/internal/body.tssrc/utils/internal/validate.tssrc/utils/json-rpc.tstest/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]>
Update: inlined the limiter, dropped the srvx/body-limit dependencyTwo rounds of cleanup on the error handling:
Net effect vs. the earlier commits: no srvx version bump (reverted — Still green: 1596 passed, lint + typecheck clean, no unhandled rejections. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)
40-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse 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
📒 Files selected for processing (1)
src/utils/internal/body.ts
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]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/utils/internal/body.ts (1)
31-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse options objects for both limiting helpers.
src/utils/internal/body.ts#L31-L70: ChangelimitBody(body, limit)to accept{ limit }.src/utils/internal/body.ts#L95-L95: ChangelimitRequestBody(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
📒 Files selected for processing (3)
docs/2.utils/1.request.mdsrc/utils/body.tssrc/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
| if (bodyReadMethods.has(prop as any)) { | ||
| return () => (limitedBody() as any)[prop](); | ||
| } | ||
| return Reflect.get(target, prop); |
There was a problem hiding this comment.
🩺 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.tsRepository: 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.tsRepository: 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().
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]>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Closes #1498.
What
bodyLimit()/assertBodySize()verified request body size by draining a cloned request stream before the handler ran — adding latency, holding up tolimitbytes in memory per in-flight request, and blocking streaming handlers. This switches to streaming enforcement using srvx's pull-based limiter (now publicly exported atsrvx/body-limit), keeping the exact same byte-accurate guarantee with no pre-buffering.How
assertBodySizewrapsevent.req.bodywithlimitBodyStream(...)and swaps in a rebuiltRequest, so bytes are counted as the handler reads them — noclone(), no pre-buffer, streaming handlers start immediately.limitRequestBodycan't be used directly: its internalnew Request(request, ...)throws on srvx's NodeServerRequest(Cannot read private member #state). Instead we wrapreq.bodywithlimitBodyStreamand rebuild a nativeRequest, re-attaching srvx's runtime augmentation (runtime/waitUntil/ip/context) — mirroringcreateSubRequestinproxy.ts. Verifiedevent.runtimeandgetRequestIPkeep working after the swap.413for an honest oversizedContent-Length, and400forContent-Length+Transfer-Encoding(RFC 7230 smuggling).assertBodySizeis now synchronous (void) — no draining left to await.ERR_BODY_TOO_LARGE. It's mapped to a handled413intoResponse(so it isn't logged as an unhandled server error), andreadBody'sformData()catch re-throws it instead of masking it as400.Trade-offs (documented in JSDoc)
413(the fail-fast413is kept wherever an honestContent-Lengthis present).Notes / things to review
^0.12.2(adds thesrvx/body-limitentry point).7550 → 7600bytes (3000 → 3050gzip): theERR_BODY_TOO_LARGEcheck sits in thetoResponsehot path (~13B). Flagging for a conscious ok.formData()integration test is node-target only: an in-processFormDatabody 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) + rewrittentest/unit/body-limit.test.tscover: fail-fast413, chunked overflow surfacing mid-stream, lying-smallContent-Length, CL+TE400,formData→413mapping, and an unread body not being counted. Full suite: 1592 passed, lint + typecheck clean.🤖 Generated with Claude Code
Summary by CodeRabbit
413 Request Entity Too Large.Content-LengthandTransfer-Encodingscenarios to avoid request smuggling.