Skip to content

fetch: hand off buffered response bodies instead of copying them - #31676

Open
alii wants to merge 11 commits into
mainfrom
ali/fetch-body-buffer-swap
Open

fetch: hand off buffered response bodies instead of copying them#31676
alii wants to merge 11 commits into
mainfrom
ali/fetch-body-buffer-swap

Conversation

@alii

@alii alii commented Jun 1, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Removes one to two full-body copies from every buffered fetch() response.

1. Buffer handoff in FetchTasklet::callback. Each HTTP-thread delivery copied the accumulated response bytes into scheduled_response_buffer. For the final delivery into an empty scheduled buffer — the common buffered case, where one callback carries the complete body — the two vectors are now swapped instead. Intermediate streaming chunks keep the copy (so response_buffer retains its capacity for reuse across chunks), and close-delimited bodies that deliver across multiple callbacks keep appending. Both sides of the handoff stay under the existing tasklet mutex.

2. Direct gzip decompression for all sizes. The libdeflate path decompressed bodies up to 512KB (decompressed) into the HTTP thread's shared scratch buffer and then copied the result into the response buffer. The >512KB branch already used the gzip ISIZE trailer to reserve the exact size and decompress directly into the response buffer; this extends that to all sizes. Safety unchanged:

  • the reservation stays capped at 32MB (ISIZE is attacker-controlled);
  • an ISIZE too small for the actual data makes decompress_to_vec fail with InsufficientSpace and fall through to the existing streaming slow path (and a corrupted ISIZE is an integrity error both libdeflate and zlib reject — unchanged);
  • the multi-chunk paths set is_libdeflate_fast_path_disabled before any accumulation, so this branch only ever sees whole bodies with an empty output buffer.

Measured (release builds of the same commit, local server, fixed-work loop, macOS arm64; medians of 2 runs)

case (fetch().arrayBuffer()) main this PR delta
gzip 256KB 60 µs CPU/op, 16.6K ops/s 53 µs, 18.8K ops/s −12% CPU, +13% throughput
gzip 1MB 113-115 µs, 9.3K ops/s 100-109 µs, 10.3K ops/s ~−9% CPU, +8%
plain 256KB / 1MB ~1-2% (swap only)
16KB (either) within noise

The 256KB-class gzip response is the case that moves most — exactly the newly-covered range (was scratch + copy, now direct).

Correctness

Differential-tested against the unpatched build — byte-identical results or identical error codes for: honest gzip (small/large), multi-member streams, corrupted ISIZE, corrupted CRC, truncated streams, empty gzip, uncompressed bodies, close-delimited multi-callback bodies, and streamed chunked consumption.

New behavior pins in test/js/web/fetch/fetch-gzip.test.ts (pass on released Bun and this PR): exact decode for honest/empty streams, ZlibError rejection for corrupted-ISIZE/CRC/truncated streams — the trailer cases also exercise the exact-size-reservation fallback.

Local suites on the patched debug build: fetch.test.ts, fetch-gzip.test.ts, fetch.stream.test.ts, body.test.ts, body-stream.test.ts — 9,909 pass; the 3 failures are environmental (two external-network TLS tests and one whose C helper server cannot start in this environment; all three fail identically on released Bun here).

Two full-body copies on the buffered fetch response path are avoidable:

- FetchTasklet::callback copied the accumulated response bytes into
  scheduled_response_buffer on every delivery. For the final delivery
  into an empty scheduled buffer - the common buffered case, where one
  callback carries the complete body - swap the two vectors instead.
  Intermediate streaming chunks keep the copy so response_buffer retains
  its capacity for reuse, and close-delimited bodies that deliver across
  multiple callbacks keep appending.

- The libdeflate gzip path decompressed bodies up to the shared scratch
  buffer's size (512KB) into that scratch and then copied the result into
  the response buffer. Use the gzip ISIZE trailer to reserve the exact
  size and decompress directly into the response buffer for all sizes
  (the >512KB branch already worked this way). The reservation stays
  capped at 32MB; a trailer too small for the actual data makes
  decompress_to_vec fail with InsufficientSpace and fall through to the
  existing streaming slow path, and the multi-chunk paths still disable
  this fast path before any accumulation happens.

Differential-tested against the previous behavior: honest gzip
(small/large), multi-member, corrupted ISIZE/CRC, truncated streams,
empty gzip, uncompressed, close-delimited multi-callback bodies, and
streamed chunked consumption all produce byte-identical results or the
same error codes.
@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator
Updated 6:38 PM PT - Jun 9th, 2026

@robobun, your commit 0dbbda9 is building: #61640

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Decompression error: ZlibError - empty chunked gzip response breaks fetch() #23149 - Empty chunked gzip response causes ZlibError in fetch(); this PR extends the libdeflate decompression fast path and adds explicit edge-case tests for empty gzip responses

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #23149

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fetch: move scheduled_response_buffer into ByteStream as Owned to skip per-chunk copy #31126 - Both optimize scheduled_response_buffer in FetchTasklet.rs to eliminate response body copies; fetch: move scheduled_response_buffer into ByteStream as Owned to skip per-chunk copy #31126 targets the streaming path with mem::take, while this PR targets the buffered delivery path with mem::swap

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a63bccc0-2ded-4854-a328-644804fced71

📥 Commits

Reviewing files that changed from the base of the PR and between 53f2bc7 and 9c791c8.

📒 Files selected for processing (1)
  • test/js/web/fetch/fetch-gzip.test.ts

Walkthrough

Tightens libdeflate gzip fast-path eligibility and acceptance, optimizes fetch callback buffering to avoid copies on final responses, and adds deterministic tests covering gzip corruption, truncation, and ISIZE trailer edge cases.

Changes

Gzip decompression and fetch response handling

Layer / File(s) Summary
Gzip decompression fast-path constraint and documentation
src/http/InternalState.rs
Require estimated_size > 0 and < 32 MiB for the libdeflate direct-decompress fast path; document the 32 MiB reservation cap and fallback conditions; accept fast-path only when libdeflate consumes all input and shrink oversized capacity; clear/shrink partial output on fallback.
Response buffer accumulation optimization
src/runtime/webcore/fetch/FetchTasklet.rs
On final successful fetch results, swap response_buffer.list into scheduled_response_buffer.list when the target is empty to avoid copying; otherwise retain the previous copy-based behavior, then reset response_buffer for reuse.
Gzip response edge-case test suite
test/js/web/fetch/fetch-gzip.test.ts
New tests exercise deterministic gzip payloads, corrupted/truncated ISIZE and CRC scenarios, multiple-member ISIZE interactions, and a subprocess RSS check for repeatedly fetching a gzip with a lying large ISIZE.

Suggested reviewers:

  • cirospaciari
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: optimizing buffered fetch response handling by eliminating unnecessary copies through vector swapping instead of copying.
Description check ✅ Passed The description thoroughly covers both objectives with detailed explanations, performance measurements, correctness validation, and testing strategy as required by the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/js/web/fetch/fetch-gzip.test.ts`:
- Around line 328-330: The test currently uses expect(async () => { await (await
fetch(server.url)).arrayBuffer(); }).toThrow(), which doesn't catch Promise
rejections; replace it with an async-rejection assertion such as awaiting the
rejected promise: await expect(fetch(server.url).then(r =>
r.arrayBuffer())).rejects.toThrow(); to ensure the arrayBuffer() rejection is
actually asserted (referencing fetch, arrayBuffer, and server.url in the test).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7019b29c-cc2a-48eb-b60b-49a9ed8c60fa

📥 Commits

Reviewing files that changed from the base of the PR and between 90f334a and 582caf3.

📒 Files selected for processing (3)
  • src/http/InternalState.rs
  • src/runtime/webcore/fetch/FetchTasklet.rs
  • test/js/web/fetch/fetch-gzip.test.ts

Comment thread test/js/web/fetch/fetch-gzip.test.ts Outdated
Comment thread test/js/web/fetch/fetch-gzip.test.ts Outdated
@alii

alii commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

✅ Adopted; current state:

CI on build 61640: all 13 red jobs are build steps that expired waiting for agents (whole-fleet backlog during the window; nothing ran and failed). A Buildkite rebuild once agents recover is all that's needed. Awaiting maintainer judgment on the complexity-vs-win tradeoff Jarred raised.

robobun added 2 commits June 2, 2026 21:19
…lback

The error assertions now await the full fetch+read chain with
.rejects.toThrow("ZlibError") — the rejection surfaces from fetch()
itself for fully-buffered bodies, which the arrayBuffer()-only form
would leak.

Rename isize-corrupt to isize-oversized (its ~4.28 GB trailer is
rejected by the 32 MB cap and never enters the exact-size reservation)
and add isize-undersized, whose in-cap but too-small trailer makes
decompress_to_vec fail with InsufficientSpace and fall through to the
streaming slow path.
Bun.listen({ hostname: "localhost" }) can bind only ::1 while fetch()
resolves localhost to 127.0.0.1, making the test fail with
ConnectionRefused depending on the environment's resolver ordering.
Pin both sides to the same loopback address.
@robobun
robobun force-pushed the ali/fetch-body-buffer-swap branch from 21a84d7 to 8a8dcd7 Compare June 2, 2026 21:31

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the memory lifetime of all the fetch stuff is very confusing right now and I am worried about increasing the complexity.

The reason for the cloning here, originally, was worries about memory fragmentation and cross-thread memory reclamation being worse. That did get better in mimalloc v3. But I would be curious about what RSS looks like when run for ~5 minutes in a release build with a request & response throughput heavy benchmark for both small responses and larger responses both with gzip and without

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found and prior feedback is addressed, but this swaps buffer ownership in the fetch() hot path and widens how attacker-controlled gzip ISIZE trailers gate direct decompression — worth a human sign-off given every buffered response flows through here.

Extended reasoning...

Overview

Two production changes plus tests:

  • FetchTasklet::callback (src/runtime/webcore/fetch/FetchTasklet.rs): when the final HTTP-thread delivery lands in an empty scheduled_response_buffer, mem::swap the two Vec<u8>s instead of copying. The post-swap response_buffer.reset() becomes a no-op on the swapped-in empty Vec; both sides remain under the existing tasklet mutex.
  • InternalState::decompress_bytes (src/http/InternalState.rs): the libdeflate direct-decompress branch now triggers for any ISIZE in (0, 32MB) rather than only (shared_buffer.len(), 32MB). Failure (InsufficientSpace/BadData) still falls through to the streaming slow path via the existing break 'libdeflate.
  • New edge-case tests in fetch-gzip.test.ts covering honest/empty/corrupted-trailer/truncated streams, plus a pre-existing localhost→127.0.0.1 flake fix.

Security risks

The ISIZE trailer is attacker-controlled. The 32MB reservation cap is unchanged and the new > 0 lower bound prevents zero-size reservations. A lying ISIZE either exceeds the cap (skips the branch) or under-reserves (libdeflate fails, falls through to zlib). This is the same safety envelope as the pre-existing >512KB branch, just applied more broadly — but it does mean the direct-into-caller-buffer path now runs for essentially every buffered gzip response rather than only large ones, so any latent issue in that path gets much wider exposure.

Level of scrutiny

High. FetchTasklet::callback runs for every fetch() response, and the swap changes which allocation each MutableString owns across the HTTP-thread → JS-thread handoff. The reasoning looks sound (final delivery, empty target, under mutex, InternalState.body_out_str still points at the same MutableString struct whose inner Vec was swapped), and the PR was differential-tested for byte-identical output — but this is exactly the kind of ownership change where a maintainer familiar with the tasklet lifecycle should confirm no path reads response_buffer after the swap expecting the body bytes.

Other factors

Both prior review threads (CodeRabbit's async-rejection assertion and my ISIZE-byte-offset coverage note) were addressed in 4adc39b and are resolved. CI build #60009 reports failures that robobun attributes to unrelated lanes (docker-build-on-macOS, sql, terminal, autobahn). A maintainer (alii) is already engaged via @robobun adopt. No CODEOWNERS entries cover these files.

@alii

alii commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

Re the possible-duplicate flag for #31126: they're complementary, not duplicates. #31126 removes the per-chunk copy on the streaming path (scheduled_response_bufferByteStream.buffer, by passing the allocation as Owned); this PR removes the copy on the buffered path (response_bufferscheduled_response_buffer, by swapping on final delivery) plus the gzip shared-scratch copy. A response takes one path or the other, so the two changes compose — landing both removes one copy from each consumption mode. They touch nearby code in FetchTasklet.rs, so whichever merges second needs a trivial rebase.

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Ran the RSS soak you asked for. Setup: release builds of this PR vs its merge-base (only the two src files differ), Linux x64 container (CPU/memory-capped, shared — so absolute numbers are modest and run-to-run variance is ±5–10%). Server is a fixed system-bun process serving static bodies with Content-Length (gzip cases pre-compressed with Content-Encoding: gzip); the measured process is the client: 16 concurrent keep-alive workers doing fetch() + arrayBuffer() for 5 minutes per cell (one 10-minute run noted below). RSS self-sampled at 1 Hz; VmHWM from /proc; slope is a least-squares fit over the post-warmup samples.

scenario build ops/s RSS start→end (MB) peak sampled VmHWM slope MB/min
plain 2KB (r1) base 80.2K 44 → 45 48 52 +0.2
PR 75.6K 46 → 41 48 53 −0.8
plain 2KB (r2) base 79.4K 48 → 42 50 52 −0.1
PR 83.3K 47 → 42 47 52 −0.1
plain 1MB (r1) base 2.81K 81 → 77 95 115 −0.1
PR 2.36K 82 → 89 138 154 +5.6
plain 1MB (r2) base 2.59K 79 → 73 129 131 −1.3
PR 2.72K 90 → 69 129 129 −7.2
plain 1MB, 10 min PR 2.90K 82 → 64 94 117 −1.0
gzip 2KB base 51.3K 44 → 41 46 52 −0.2
PR 69.6K 47 → 45 48 52 +0.1
gzip 256KB base 12.2K 69 → 70 74 105 −0.6
PR 14.6K 67 → 69 74 74 −0.5
gzip 1MB base 4.14K 58 → 82 107 107 −0.6
PR 5.60K 76 → 68 100 100 −1.1

RSS: no divergence attributable to the PR. Start/end/peak all land inside the baseline's own run-to-run envelope for every scenario, slopes oscillate around zero for both builds (sawtooth reclamation, not monotonic growth), and the longest run — 10 minutes of sustained 2.9 GB/s plain 1MB on the PR — ends below where it started (64 MB) with a 117 MB high-water mark, inside the baseline's 115–131 range. The one scary-looking cell (plain-1MB r1 PR: +5.6 MB/min, HWM 154) did not reproduce in the rerun or the 10-minute soak; the baseline produced a similar outlier peak (131) in its own rerun, so I read both as container noise.

Throughput: gzip is where the PR moves — +36% (2KB), +19% (256KB), +35% (1MB) in these runs, consistent with the direct-decompress path removing the scratch-buffer copy. Plain is equivalent within noise (r1 −6%/−16%, r2 +5%/+5% — the swap only saves one memcpy there).

Caveats worth stating: the gzip payloads are synthetic/highly compressible, so those scenarios are decompression-heavy but light on wire I/O; and this container's variance means the plain-throughput deltas shouldn't be read as anything but "no change". Raw per-run JSON (all 15 runs): happy to paste if useful.

On the cross-thread reclamation worry specifically: the handed-off buffer (allocated on the HTTP thread, freed on the JS thread after consumption) is the same cross-thread free the copied scheduled_response_buffer already performed — the swap changes which allocation crosses, not whether one does. The steady-state RSS above is consistent with that.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing the ISIZE test-coverage feedback — the split into isize-oversized/isize-undersized now exercises each reservation-branch exit precisely. I didn't find any correctness issues in this revision, but deferring to a human since this touches cross-thread buffer ownership in the core fetch path and widens the attacker-influenced ISIZE allocation branch, and the sustained-RSS benchmark results are still pending.

Extended reasoning...

Overview

This PR removes one to two full-body copies from buffered fetch() responses via two changes: (1) in FetchTasklet::callback, swap response_bufferscheduled_response_buffer on the final delivery into an empty scheduled buffer instead of copying; (2) in InternalState::decompress_bytes, widen the libdeflate direct-decompress branch from estimated_size > shared_buffer.len() to estimated_size > 0, so all sub-32MB gzip bodies decompress straight into the caller's buffer rather than via the shared scratch buffer + copy. New tests in fetch-gzip.test.ts pin honest/empty/corrupted-trailer/truncated behavior.

Security risks

The ISIZE trailer is attacker-controlled, and this change makes the direct-allocation branch fire for any nonzero ISIZE under 32 MB (previously only when ISIZE exceeded the ~512KB shared buffer). The 32 MB cap is unchanged and the fallback on InsufficientSpace/BadData is preserved, so I don't see a new DoS or memory-safety vector — but it does change how often and how much we allocate based on untrusted input, which deserves human eyes. The buffer swap is under the existing tasklet mutex and the post-swap response_buffer.reset() is harmless on the now-empty vector.

Level of scrutiny

High. Both touched paths are in the hot, every-request fetch pipeline: one alters cross-thread buffer ownership semantics (HTTP thread → JS thread handoff), the other alters allocation sizing driven by untrusted gzip trailers. The reasoning in the PR description is careful and the differential testing is thorough, but this is exactly the kind of change where a maintainer familiar with the response_buffer reuse lifecycle and the libdeflate/zlib fallback contract should sign off.

Other factors

My earlier inline comment (the original isize-corrupt case skipped the reservation branch entirely) was fully addressed — the test now has separate oversized/undersized cases with an accurate comment describing each exit path. CodeRabbit's async-assertion nit was also resolved. Two things keep this from auto-approval: robobun is still running the sustained-RSS benchmark that was requested, and CI flags an unexplained ~1 MB binary-size increase on every aarch64 target for what is a very small source diff — both worth a human glance before merge.

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Two quick notes on the open review points: the sustained-RSS benchmark is done — results in #31676 (comment) (no RSS divergence over 5–10 min soaks; gzip +19–36% throughput, plain unchanged). And the "~1 MB aarch64 binary-size increase" was measured on the pre-merge build (#60010, since canceled), whose canary baseline had drifted — main picked up a WebKit upgrade after this branch forked, which a ~30-line Rust diff can't account for. The current build on the merged-with-main head re-measures against an aligned baseline.

robobun and others added 3 commits June 3, 2026 06:14
…-swap

# Conflicts:
#	test/js/web/fetch/fetch-gzip.test.ts
…ling data

The reservation fast path reads the buffer's last 4 bytes as the gzip
ISIZE trailer. With trailing data after the stream those bytes are
attacker-chosen: a tiny body could reserve up to the 32 MB cap, and the
oversized Vec is later adopted as-is into JS objects whose GC accounting
sees only len - the capacity is invisible to the collector and
accumulates across requests.

Treat a decode that does not consume the whole buffer as a miss for the
fast path (the streaming slow path already handles trailing data the
same way released versions do), and shrink grossly oversized
reservations - capacity more than twice len with at least 64 KB of
excess - before the buffer leaves the HTTP layer.

Tests: trailing junk spelling a huge ISIZE must decode the real body
with bounded RSS across 64 requests; gzip with trailing data decodes
without corruption, matching released behavior.
Comment thread src/http/InternalState.rs
Comment thread test/js/web/fetch/fetch-gzip.test.ts
The shrink added in 01dbffb sat inside the read == buffer.len() branch,
where libdeflate has just verified the trailer equals the output length,
so capacity never grossly exceeds len there. The lying-trailer case it
was written for takes the other exit: Success with trailing data skips
the shrink, the slow path decodes into the same Vec without
reallocating, and the swap hands up to 32 MB of capacity to JS behind a
tiny body. The reservation's pages are never touched, so RSS cannot see
it; VmSize grows ~34 MB per held response.

Clear the partial output and drop oversized capacity on that exit
instead, and keep the success-path shrink for capacity carried over
from a reused connection.

The subprocess test now measures the VmSize delta on Linux (fails at
2198 MB without this fix, 517 MB with it) and drains stderr so a child
failure surfaces the real error instead of an empty-JSON parse error.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/js/web/fetch/fetch-gzip.test.ts`:
- Around line 363-366: The test binds the server with raw.listen(0) which can
pick an IPv6 unspecified address; update the listen call to bind explicitly to
127.0.0.1 (e.g., raw.listen(0, "127.0.0.1", ...)) so the subsequent fetch to
http://127.0.0.1:${port}/ connects reliably; keep the rest of the logic that
reads port via raw.address() and the fetch invocation unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86be8c6b-7d0c-4489-b500-c2d8e37caff5

📥 Commits

Reviewing files that changed from the base of the PR and between f1322c4 and 53f2bc7.

📒 Files selected for processing (2)
  • src/http/InternalState.rs
  • test/js/web/fetch/fetch-gzip.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 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 `@test/js/web/fetch/fetch-gzip.test.ts`:
- Around line 363-366: The test binds the server with raw.listen(0) which can
pick an IPv6 unspecified address; update the listen call to bind explicitly to
127.0.0.1 (e.g., raw.listen(0, "127.0.0.1", ...)) so the subsequent fetch to
http://127.0.0.1:${port}/ connects reliably; keep the rest of the logic that
reads port via raw.address() and the fetch invocation unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86be8c6b-7d0c-4489-b500-c2d8e37caff5

📥 Commits

Reviewing files that changed from the base of the PR and between f1322c4 and 53f2bc7.

📒 Files selected for processing (2)
  • src/http/InternalState.rs
  • test/js/web/fetch/fetch-gzip.test.ts
🛑 Comments failed to post (1)
test/js/web/fetch/fetch-gzip.test.ts (1)

363-366: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bind the raw server to 127.0.0.1 explicitly.

This test fetches 127.0.0.1, but raw.listen(0) can bind the unspecified IPv6 address on some hosts. That makes the case intermittently fail with ECONNREFUSED for the same reason called out on Line 201.

Proposed fix
-      await new Promise<void>(resolve => raw.listen(0, () => resolve()));
+      await new Promise<void>(resolve => raw.listen(0, "127.0.0.1", () => resolve()));
🤖 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 `@test/js/web/fetch/fetch-gzip.test.ts` around lines 363 - 366, The test binds
the server with raw.listen(0) which can pick an IPv6 unspecified address; update
the listen call to bind explicitly to 127.0.0.1 (e.g., raw.listen(0,
"127.0.0.1", ...)) so the subsequent fetch to http://127.0.0.1:${port}/ connects
reliably; keep the rest of the logic that reads port via raw.address() and the
fetch invocation unchanged.

A bare listen(0) can bind only the IPv6 unspecified address on some
hosts while the test fetches 127.0.0.1, the same mismatch already
pinned for the TCP-server test in this file.
@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun fix review comments

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@alii every review comment visible via the API is addressed at head (9c791c8), and all 7 inline threads are resolved:

  • claude[bot], shrink on the wrong branch (the lying-ISIZE reservation survived into JS): fixed in 53f2bc7, the miss exit now discards the partial output and drops capacity over 64 KB. Verified by VmSize: 2198 MB retained across 64 held bodies unfixed, 517 MB fixed, and the subprocess test now asserts that delta on Linux.
  • claude[bot], stderr piped but never read in the RSS test: fixed in 53f2bc7 (drained in the Promise.all, asserted empty before parsing stdout).
  • coderabbit, expect(async...).toThrow() form: switched to await expect(fetch(url).then(r => r.arrayBuffer())).rejects.toThrow("ZlibError") in 4adc39b (the suggested (await fetch()).arrayBuffer() form would leak the rejection, which surfaces from fetch() itself for buffered bodies).
  • claude[bot], ISIZE trailer coverage: isize-oversized/isize-undersized split in 4adc39b, each exit verified against an instrumented build.
  • coderabbit, raw.listen(0) IPv6 bind in the empty-compressed tests: pinned to 127.0.0.1 in 9c791c8 (and the TCP-server test in 8a8dcd7).
  • Jarred's RSS question: answered with the soak benchmark, fetch: hand off buffered response bodies instead of copying them #31676 (comment).

If you're seeing comments beyond these, they may be sitting in a pending review that hasn't been submitted, since nothing else shows up on the reviews, review-comments, or threads endpoints. Happy to pick them up once they're visible.

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.

3 participants