Repository navigation
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review. WalkthroughByteStream now stores terminal errors separately from its pending read result. JavaScript errors use a cached ChangesByteStream Error Handling
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The reviewed change has no established merge-blocking behavior. The inspected producer paths create the readable wrapper before an error can be delivered. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Status Reproduced with the script in the PR description: With this branch (debug build): Test proof:
CI (build 122408 on 8d54498): 180 of 181 jobs pass. The one red test is PR: #43410 |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked the sibling ReadableStreamPtr::File arm in src/runtime/webcore/s3/client.rs (still reads pending.result directly) for the same rooted-cycle class — FileReader only ever parks native StreamError::Error values there, never a JS value, so it is not affected. The other candidate examined (a JS error dropped as PendingError::None when the wrapper is already gone at ByteStream.rs:743) was ruled out because no reader can reach the stream without the wrapper.
Extended reasoning...
Three confirmed findings are already posted inline, so this body only records what else was examined. The FileReader arm of upload_stream was left on the old pending.result replace/reset pattern; that is fine because FileReader.rs stores only StreamError::Error(sys::Error) in pending.result, which is not a GC root and cannot close a cycle through a Response. The wrapper-gone drop in set_pending_error is benign since every consumer (on_pull, to_buffered_value, wire_native_sink, the S3 check) reaches the ByteStream through its wrapper cell.
|
Updated 11:36 AM PT - Oct 1st, 2026
✅ @robobun, your commit ec56c3a85869819428276cc92070e3bbc780b4a1 passed in 🧪 To try this PR locally: bunx bun-pr 43410That installs a local version of the PR into your bun-43410 --bun |
…#43699) ### Problem - A `fetch()` is aborted while its body still arrives. A native reader of `res.body` does not get `signal.reason`, as it does on 1.3.14 and Node. One that already reads gets a fresh `AbortError: The operation was aborted.`. One that starts later gets a clean, empty body: `text()` gives `""`, `Bun.serve` answers 200. - The cause is `ReadableStream::error` (`src/runtime/webcore/ReadableStream.rs:291`), called by `BodyAbortListener::on_abort` since #35093. It errors the JS stream, then cancels the `ByteStream`. `ByteStream::on_cancel` ends a native reader with a generic `UserAbort`, and a later one never looks at the JS stream. ### Fix - `ReadableStream::error` fails what reads natively right now (a wired sink, a buffer action, a parked pull) with the reason. The reason is never kept natively: a `Strong` roots a reason that references its own `Response`. - An errored JS stream hides its native source (`nativePtrForJS()`, the Rust tag), so a later reader gets the stored error. - `FileReader` gets the same hook, and its `on_cancel` now fails a sink that is still wired (3980be6). No caller of `error` holds a File source. - Verified: `test/js/web/fetch/fetch-abort-stream-body.test.ts` (68 new cases, main fails 55). Suites: see Notes. ### Background - A `ByteStream` is the native source of a streamed body. The fetch tasklet pushes chunks, or one terminal error, into it. - A native reader takes the bytes with no JS reader: a buffer action (`new Response(res.body).text()`), a wired sink (`Bun.write`, a fetch upload, `HTMLRewriter`, `Bun.serve`), or `Readable.fromWeb()`. - `BodyAbortListener` is a fetch `Response`'s `AbortSignal` listener. It errors the body on abort. <details><summary>Notes</summary> **Repro (from the report).** A server sends a head and 40 of 100 body bytes, then stalls. `AbortSignal.timeout(150)` or `ac.abort(new Error("custom"))` fires. | reader of `res.body` | main, reader started before the abort | main, reader starts after the abort | this PR, both | | --- | --- | --- | --- | | `res.text()`, reader loop, `pipeTo`, `pipeThrough`, `tee` | `signal.reason` | `signal.reason` | `signal.reason` | | `new Response(res.body).text()` / `json` / `bytes` / `blob` / `arrayBuffer` | fresh `AbortError` | resolves `""` / empty | `signal.reason` | | `new Request(url, { body: res.body }).text()` | fresh `AbortError` | resolves `""` | `signal.reason` | | `Bun.readableStreamToText` / `Bytes` / `ArrayBuffer` / `JSON` / `Blob`, `res.body.text()` | fresh `AbortError` | resolves `""` / empty | `signal.reason` | | `Bun.write(path, new Response(res.body))` | fresh `AbortError` | resolves `0` | `signal.reason` | | `fetch(url, { method: "POST", body: res.body })` | fresh `AbortError` | uploads an empty body, resolves | `signal.reason` | | `new HTMLRewriter().transform(new Response(res.body)).text()` | fresh `AbortError` | resolves `""` | `signal.reason` | | `Readable.fromWeb(res.body)` | `end`, as if the body were complete | `end` after 0 bytes | `signal.reason` (see the idle case below) | | `Bun.serve` handler returns `new Response(res.body)` | reports the generic `AbortError`, cuts the connection | 200 with an empty body | reports `signal.reason`, cuts the connection | **History of this PR.** The first commit handled only a reader that already reads. The review found the late reader (second column). The second commit stored the reason in the ByteStream for it. The review then found that this `Strong` is a GC root: with `abort(new Error("x", { cause: res }))`, `res.body` touched and `await res.text().catch(() => {})`, main collects every `Response` and that head kept 200 of 200. The last source commit stores nothing and hides the native source of an errored stream instead. The `ByteStream::to_any_blob` and `Bun.serve` changes of the second commit are gone. **The idle `Readable.fromWeb()`.** It already holds the native source, so the errored JS stream cannot stop it, and it has no pull parked, so nothing can take the reason when the abort fires. It now fails with a generic `AbortError` on its next pull. On main it ends cleanly. It cannot get the reason without a traced slot for it. #43410 adds such a slot (`pendingError`). **The `FileReader` commit (3980be6).** It mirrors `ByteStream`: `error_native_consumer` for a wired sink or a parked pull, and `on_cancel` ends a sink that is still wired with `UserAbort`, so the sink does not wait for an end that never comes. `ReadableStream::error` has two callers (`Response.rs:155`, `Body.rs:1374`) and neither holds a File source, so the `error` arm is parity only and no public API can test it. **Other notes.** - A `Bun.serve` response of an errored stream is reported on the server and the connection is cut. That is what `Bun.serve` does for any JS stream that errors, also before the first byte. This PR does not change that policy. - `res.body; await res.text()` is not affected on main: the body mixin drops `Locked.readable` once it starts the buffer action, so `on_abort` finds no stream and the tasklet delivers the reason. - Primitive reasons (`null`, `42`, `"str"`, a symbol) and the default `abort()` reason reach the reader unchanged. The test covers them with the abort before and after the reader starts. - Node v26.3.0 rejects the upload case with `TypeError: fetch failed` and `cause === signal.reason`. Bun rejects an upload with the body stream's error itself. This PR keeps that and restores the 1.3.14 result. - Heap probes (200 aborted fetches each: body never read, read late, parked reader, and a reason that references its `Response` followed by `res.text()`) retain the same as main. A reason that references its `Response` is retained on main already when the body is never read, through the body's `Value::Error`. #43521 addresses that. - Not covered here, reported separately: a body that fails with no abort (a connection reset) before the reader starts. On main a late JS reader gets a clean `done` (`ByteStream::on_start` ignores the stored error), `Bun.write(path, new Response(res.body))` resolves `0`, and a `Bun.serve` handler that returns `new Response(res.body)` answers 200 with an empty body (`ByteStream::to_any_blob` and the `Bun.serve` ByteStream path ignore it too). - Suites run on a debug build: `fetch-abort-stream-body`, `fetch-backpressure`, `body-stream`, `body`, `body-clone`, `body-mixin-errors`, `fetch-stream-cancel-leak`, `fetch-cyclic-reference`, `fetch-leak -t abort`, `streams`, `native-source-onclose-leak`, `sync-pull-fast-path`, `node-stream`, `html-rewriter`, `html-rewriter-leak`, `proxy-stress-lifecycle`, `proxy-stress-errors`, `async-iterator-stream`, `serve`, and the node `test-webstreams-*` / `test-whatwg-readablestream*` / `test-*readable-from-web*` files. An earlier head of the test file also passed on a Windows x64 debug build. - Local failures that also fail without this change: `serve.test.ts` "root range port" and "/bun:info to loopback clients" (the container runs as root), `fetch-leak.test.ts` "Sending URLSearchParams does not leak" (120 s timeout), some `fetch.stream.test.ts` "Content-Length response works (multiple parts)" cases (5 s timeout). </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/web/fetch/fetch-abort-stream-body.test.ts <!-- robobun:evidence:end --> --------- Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
…rapper A producer can deliver its terminal error while nothing can take it: no sink, no buffer action, no pending pull. ByteStream kept that error in a Strong until the next read. A Strong is a GC root, so an error that references the Response that owns the stream closed a cycle through a root, and none of it was ever collected. The JS error now sits in a new visited pendingError slot of the Bytes source wrapper. A native error stays native. Every reader goes through take_pending_error().
…urvives a collection
e457f43 to
9cbc99a
Compare
Also drops one sentence in error_native_consumer: a JS reason that waits in the pendingError slot is not a GC root.
…with the buffer (#38003) ### Problem - A native byte stream first read after its producer failed ends clean after 0 bytes. Sources: `fetch` body, `req.body`, `S3File.stream()`, `HTMLRewriter` output. `Bun.serve` answers an empty `200`. An S3 copy stores an empty object. - If a reader took part of the buffer, the process aborts: `panic: range end index 262144 out of range for slice of length 0` (`ByteStream::take_buffer`). - Cause: `ByteStream::append(Err)` stores the error, drops `buffer` and keeps `offset`. `on_start`, `to_any_blob` and `Bun.serve` read that as "ended, empty". ### Fix - A stored error makes `on_start` return `Start::Ready` and `to_any_blob` return `None`, so the first pull delivers it. `Bun.serve` hands it to its sink, and `end_chunk` calls `error()`. - `buffer` and `offset` become one `Buffered` value. Every method that empties the bytes resets the index. - Verified: six test files (Notes). 24 new tests fail without the change. - Self-reviewed: 2 concerns raised, 2 addressed. ### Background - `ByteStream` is the native source of a streamed body. - A pull copies into a view. Bytes that do not fit stay buffered. `offset` counts the ones already taken. - Weighed a store in `append(Err)` and a clamp in `take_buffer`. Six sites empty the buffer, and a clamp hides a wrong index. ### Downsides - A handler that returned such a body got a silent `200`. It now gets `error()` or the default 500. - One test of `pending.result` per stream start and per `Bun.serve` attach. Release `text`: +768 bytes. - Still open: `Bun.write(path, stream)` rejects after it truncated the file. A started body that did not fail loses its queued bytes (#33461, #42281). <details><summary>Notes</summary> **Origin and reach.** No user reported either symptom. A fuzzer found the panic, and a differential run against node v26.3.0 found the empty body. The panic ships since 1.4.0: #38656 gave `take_buffer` its `drain(..consumed)`. The review thread at #43410 (comment) named the stale `offset` on 09-19 and got the answer that the crash would have its own change with its own test. This is that change, inside this PR so that `ByteStream.rs` has one pull. **Three routes reach `take_buffer` with an index left behind an emptied buffer.** 1. `Bun.serve` renders a `Source::Bytes` body that a consumer started and let go (`res.body.getReader().releaseLock()`), after the producer failed. `do_render_with_body` calls `drain()`. 2. `Readable.fromWeb()` on such a stream. `on_start` took the buffer and kept the index, then `native-readable.ts` calls `drain()`. This one needs no failure. 3. The stream's own pull loop in text mode (`res.textStream()`). The adapter keeps its pull view there, so `nativeSourcePullImpl` (`BunStreamSource.cpp`) calls `handle.drain()` before each pull. A body that fails after a partial read is enough. No second consumer is involved. Sites that emptied `buffer` and kept `offset` on main: `append(Err)`, the buffer-action error arm of `on_data`, `on_cancel`, the stored-error arm of `to_buffered_value`, the `OwnedAndDone` arm of `on_start`, `to_any_blob`. `Buffered` offers `take`, `clear`, `copy_out`, `adopt` and `extend`, and nothing else writes the two fields. **Before and after** (release build of main, debug build of this branch): | script | main | this PR | | --- | --- | --- | | `HTMLRewriter` output, probed with `getReader().releaseLock()`, 400,007 bytes of output, then a handler throws, returned from `Bun.serve` | panic, exit 134 | `500`, `error()` got `boom` | | the same with exactly one pull view of output (262,144 bytes) | `200 ""` | `500`, `error()` got `boom` | | the same after only `void res.body` | `200 ""` | `500`, `error()` got `boom` | | `const body = res.body`, upstream resets, `for await (const c of body)` | ends cleanly after 0 of 100,000 bytes | rejects `ECONNRESET` (node: rejects) | | `res.textStream()` reader, one view read, upstream resets, next read | panic, exit 134 | the read rejects `ECONNRESET` | | `Readable.fromWeb()` on a probed rewriter output, clean end | panic, exit 134 | the bytes the native source still holds | | `req.body` read after the uploading client went away | `{ done: true }` | rejects `AbortError` | | `new Response(body).arrayBuffer()` / `bytes()` / `blob()` / `json()` on a body that failed while idle | resolve empty | reject `ECONNRESET` | | `new Bun.S3Client({}).file(key).stream()` (no credentials), then `for await`, a reader, `pipeTo`, `tee`, `new Response(s).bytes()` or `.blob()` | end cleanly after 0 bytes | reject `ERR_S3_MISSING_CREDENTIALS` | | `client.file(key).stream()` read after the download failed: a body cut short, a missing key (404), a denied key (403) | ends cleanly after 0 bytes | rejects `ConnectionClosed`, `NoSuchKey`, `AccessDenied` | | `client.write(key, new Response(s))` or `Bun.write(s3file, new Response(s))` with such a stream `s` | resolves 0, a `PUT` of 0 bytes replaces the object | rejects, no request goes out | | `fetch(url, { method: "POST", body: s })` | resolves 200, the server gets a complete POST with `Content-Length: 0` | rejects, the server gets no complete request | | `HTMLRewriter` output `.body` taken, a handler throws or the upstream resets, then the first read | `{ done: true }` | rejects with the handler's or the upstream's error | **Four sources, one script.** Each transfer announces 100 bytes, sends 50, and is cut by a FIN 20 ms later. The stream is taken at once and read 100 ms later: `(await fetch(url)).body`, `req.body` in `Bun.serve`, `s3.file(key).stream()`, `new HTMLRewriter().transform(await fetch(url)).body`. A release canary (367d939) and a debug build of main bd599f5 end all four cleanly with 0 bytes, 3 of 3 runs each. A debug build of this branch rejects all four, 3 of 3 runs, with the error the same read gets when it is made at once: `ECONNRESET`, `AbortError`, `S3Error ConnectionClosed`, `ECONNRESET`. **What stays open.** For a stream source, `Bun.write(path, source)` opens the destination with `O_TRUNC` before it reads the stream (`pipe_readable_stream_to_blob`, `Blob.rs:1448`). With a failed stream, or a `Response` or `Request` that wraps one, the write now rejects, and a file that held 777 bytes holds 0. Main does the same for a bare stream, and resolved 0 for the wrappers. Only `Bun.write(path, res)` with the fetch `Response` itself keeps the old bytes. An S3 destination keeps the old object, because no request goes out. This PR does not change when `Bun.write` truncates. Two behaviours change only in the state that used to abort: `on_start` and `resume` hand over the bytes no consumer took, not the whole buffer, so a prefix a reader already holds is not sent twice. The `to_write == 0` arm of `on_pull` is gone: it handled a buffer whose bytes were all taken but not dropped, which cannot exist now. The `HTMLRewriter` paces its input by the whole buffer, the taken prefix included (#38656). `Buffered::held_len()` keeps that. Counting only the untaken bytes made `html-rewriter.test.js` "a locked reader paces the input by its reads" fail, which is how the difference showed. **Self-review.** Two concerns survived: that the change may claim severity but not demand (so this body states that no user reported it and lists the routes), and that a rename in `ByteStream.rs` races the other open pulls on the file (it now ships inside this one). **Since the first revision of this PR.** Merged up to main. The `Blob.rs` hunk is gone: main rewrote `pipe_readable_stream_to_blob` and marks the pump's rejected promise handled there. `s3-write-to-file-sync-close.test.ts` now asserts the rejection instead of accepting both outcomes. `end_sink_with_pending_error` uses main's `detach_sink` plus `sink.end`. The fixture variants sit in main's `nativeBodies` table (#40596). The "failed after attach" control is dropped: main's `proxied-fetch-after-status` row covers it with #40596's contract (the connection closes without a complete message). One variant is new: the probed rewriter output from the first row above. **Tests.** `bun bd test` on Linux x64: - New or ported, failing without the change (24): `serve-stream-body-error.test.ts` (8), `body-mixin-errors.test.ts` (9), `bun-server.test.ts` (1), `fetch.stream.test.ts` (2), `s3-stream-error-gc.test.ts` (2), `html-rewriter.test.js` (2). Each was run against a debug build of main bd599f5, the first file against the release canary. - `s3-write-to-file-sync-close.test.ts` passes with and without the change: main already rejects that write, and the test now pins it. - After the rebase onto main (bd599f5): `serve-stream-body-error` 34 pass, `body-mixin-errors` 23, `s3-stream-error-gc` 4, `s3-write-to-file-sync-close` 1, `html-rewriter.test.js` 188, `bun-server` 81, `fetch.stream` 121. - This container gives the debug build about two cores. `bun-server` and `fetch.stream` were run with `--max-concurrency` lowered: with the default, their concurrent subprocess tests pass the 5 s limit, with and without the change. The "Content-Length response works (multiple parts)" rows of `fetch.stream` take 2.1 s to 4.9 s each on both builds, and in one run two of them passed the limit. - Earlier, before the rebase: `fetch-backpressure` 127, `body-stream` 9086, `body` 774, `streams` 624, `node-stream` 112. `serve.test.ts`: 325 pass. "root range port" and "/bun:info to loopback" fail on the release build of main too (the container runs as root). `bytestream-partial-read-error-fixture.ts` stages the failure with an `HTMLRewriter` whose handler throws on the second chunk, so every step runs on the JS thread in order. It waits for the handler to have run, checks that the first read is exactly one pull view, and requires the next read to reject with the handler's own error. A text-mode reader is required: a byte reader's full view retires the adapter's pending view, and the next pull then skips `drain()`. Against the released bun the fixture aborts with the panic in its header. `s3-stream-stored-error-fixture.ts` has two modes. With no credentials the download fails inside `stream()`, so every reader and sink runs against a stored failure and no timing is involved. For the failures on the wire, the stub closes the connection behind each answer. The read starts after the client has closed its end and one more request has made the round trip through the HTTP thread. The fixture writes to an absolute path: `Bun.write("out.bin", new Response(""))` with a relative path to a file that does not exist rejects with `ENOENT` from `truncate`, on main too. That is a separate defect. **Size.** `size` of the release binary: `text` 80,676,945 on main, 80,677,713 with the change. `data` and `bss` are equal. Measured before the rebase. **History.** The branch is rebased onto main at bd599f5: one commit for the change, and one for the tests of the S3 and `HTMLRewriter` sources. **Related open pulls.** #33461 and #36110 give opposite answers for a started body that did not fail (send everything, or refuse). #42281 keeps `Readable.fromWeb()` from taking a started native source. None of them touches `ByteStream.rs`. #43410 rewrites how `append(Err)` stores a JS error and needs a rebase after this. </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/workerd/html-rewriter.test.js, test/js/web/fetch/fetch.stream.test.ts, test/js/bun/http/bun-server.test.ts <!-- robobun:evidence:end -->
Problem
HTMLRewriterResponseis never collected when the handler's error references it and nothing reads its body. The Notes repro printsretained 50 of 50 protected Error: 50on main.ByteStream::append(src/runtime/webcore/ByteStream.rs:680) keeps it in ajsc::strong::Optional. AStrongis a GC root, and the error reaches it again through theResponse.Fix
pendingErrorslot (streams.classes.ts). A waiting JS error sits there, with noStrong. A native error stays native.take_pending_error(). All reach theByteStreamthrough its wrapper, so the slot lives as long as a reader can exist.test/js/workerd/html-rewriter-leak.test.ts, two leak tests fail on main (Received: 60, expected< 15). Also ran HTMLRewriter, streams, fetch, S3 suites.Background
ByteStreamis the native source of a streamed body. A producer (HTMLRewriter, fetch, S3) pushes chunks and one terminal error into it.ReadableStreamholds. Itsvaluesinstreams.classes.tsbecome visitedWriteBarrierfields.Downsides
ByteStreamgrows from 192 to 208 bytes, the debug binary by 1040. The wrapper cell stays at 80 bytes.Responsewhen nobody touched.body(Body: do not root the error of a failed body #43521).Notes
Repro 1 (
.bodymade, never read):retained 50 of 50 protected Error: 50. This branch:retained 1 of 50 protected Error: 0.text()and the like), no pending pull.void res.bodywithholder.reader = res.body.getReader()and sends a few<b>chunk</b>chunks first. The first chunk fills the reader's queue, the stream stops pulling, and the error finds no pending pull. Same numbers before and after.m_storedError.Heap snapshot (
generateHeapSnapshotForDebugging()), retainers of the waitingError:StrongRootBlock [Internal], which is a root.BytesInternalReadableStreamSource [Property: pendingError], not a root. That wrapper's retainer isReadableStream [Property: bunNativePtr].Tests (
html-rewriter-leak.test.ts, block "a handler error that nothing has read yet"):.bodymade and unread, and a reader with a backlog. 60 rewrites each, thenResponseand protectedErrorcounts fromheapStats(). With the merge base'ssrc/they fail (Received: 60, expected< 15).Strongcannot die). They check that the waiting error survives a full GC plus heap churn and reaches each reader intact:text(),Bun.readableStreamToBytes(),read()after the queued chunk, a second rewriter (wire_native_sink), and an S3 upload (the early check, zero requests made). One of them throws a string: the slot holds any value, where a weak handle holds only objects.Measurements, merge base 37471e5 against this branch, debug (ASAN) builds:
size_of::<ByteStream>()192 to 208,byte_stream::Source344 to 360.SysErroris boxed in the enum (40 bytes inline otherwise).sizeof(JSBytesInternalReadableStreamSource)72 to 80 (lldb). A JSC cell occupies a multiple of 16 bytes (MarkedBlock::atomSize), so the cell took 80 bytes before too.size build/debug/bun-debug: text +2640, data -1728, bss +128, total +1040 bytes. No release build was made.on_datadoes not change.on_pullandto_any_blobtouch the new field only after the last chunk.The fetch abort reason (#43699, merged after this PR was opened):
ByteStream, because of this root. A reader that already holds the source and pulls later (an idleReadable.fromWeb(res.body)) gets a genericAbortError. Its notes name this slot as the missing piece.reasonwould be a GC root." fromerror_native_consumer, which the slot makes false.abort(null)) thrown from the later pull closes theReadablewith no'error'event, becausedestroy(falsy)is a clean destroy in node:stream. node:stream: Readable.fromWeb emits an AbortError when the web stream fails with a falsy value #43732 maps a falsy pull error to anAbortError. The change can follow when both PRs are merged.Response: body never made, body made and never read, body made thenres.text(), reader parked, late native reader, idleReadable.fromWeb()resumed, idleReadable.fromWeb()dropped. Retained counts are the same on the merge base and on this branch (40, 40, 0 to 1, 1, 40, 0, 0). The three rows with 40 are the body'sValue::Errorroot (Body: do not root the error of a failed body #43521).Self-review:
on_pull,to_buffered_value,wire_native_sink(ReadableStream.rs) and the S3 upload check (s3/client.rs, it readpending.resultdirectly). Each one and each release site of the oldpending.resulterror (to_any_blob,on_cancel,finalize, the buffer action reject path) behaves as before.SysErroris boxed in the enum.error_native_consumerfalse. It is gone.Behaviour that does not change:
to_any_blobstill drops a waiting error, andon_startstill reportsEmptyfor it. ByteStream: report a stored producer error and keep the taken prefix with the buffer #38003 changes both. It edits the same readers, so one of the two needs a small rebase. Itshas_pending_error()becomes a check of the new field.on_cancelstill leaves a waiting error in place until the wrapper goes away.Suites run on the debug build:
html-rewriter-leak,html-rewriter,html-rewriter-end-error,fetch-abort-stream-body,body-mixin-errors,body-clone,fetch-stream-cancel-leak,fetch-response-finalizer-sweep,fetch-cyclic-reference,fetch-backpressure,streams,native-source-onclose-leak,web-stream-state,node-stream,s3-stream-error-gc,s3-upload-stream-gc,s3-stream-cancel-leak, and three node*from-web*files.Not covered here:
Body::Value::Error(ValueError::JSValue(Strong))is a second parking spot with the same cycle. A failed rewrite whose.bodynobody touched stores the handler's error there (RewriterPipe::fail, the branch without an output stream). Body: do not root the error of a failed body #43521 addresses it. This PR touches neitherBody.rsnorhtml_rewriter.rs.append(Err)clears the buffer and leavesoffsetas it was, so a latertake_buffercan panic (range end index 65536 out of range for slice of length 0). This PR does not change that path.html-rewriter-leak.test.tsrun close to the 5 s timeout on a debug (ASAN) build: "transform(new Response(ascii)) releases the input WTFStringImpl", "a worker exiting with an unobserved transform's pull queued frees the pipe" and "element.attributes iterator does not leak names/values". Each takes 2.1 to 3.5 s on an idle machine. With 24 busy loops on 16 cores, the third one timed out in 2 of 3 runs of the file. The tests this PR adds take 0.6 to 0.9 s each and passed in every such run.[human-review] gate passed · iteration 1 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 1 rejected · iteration 1
evidence per changed file