Skip to content

fetch/S3: replace ResumableSink with proper JSSinks - #36087

Merged
Jarred-Sumner merged 164 commits into
mainfrom
farm/40eaa140/fetch-node-readable-body-spin
Jul 31, 2026
Merged

fetch/S3: replace ResumableSink with proper JSSinks#36087
Jarred-Sumner merged 164 commits into
mainfrom
farm/40eaa140/fetch-node-readable-body-spin

test: inherit stderr instead of piping unused

e63fb41
Select commit
Loading
Failed to load commit list.
Claude / Claude Code Review completed Jul 27, 2026 in 27m 30s

Code review found 1 important issue

Found 4 candidates, confirmed 3. See review comments for details.

Details

Severity Count
🔴 Important 1
🟡 Nit 2
🟣 Pre-existing 0
Severity File:Line Issue
🔴 Important src/runtime/api/ResumableSink.classes.ts:27-38 controller.end(value) on the direct ResumableSink path aborts the upload instead of cleanly closing
🟡 Nit src/runtime/webcore/ResumableSink.rs:512-516 js_end/js_close clear the flushPromise slot without settling it
🟡 Nit src/jsc/bindings/webcore/streams/BunStreamSource.cpp:1564-1568 resumableSetupDirect: !pull.isCallable() branch returns without releasing/checking the ThrowScope

Annotations

Check failure on line 38 in src/runtime/api/ResumableSink.classes.ts

See this annotation in the file changed.

@claude claude / Claude Code Review

controller.end(value) on the direct ResumableSink path aborts the upload instead of cleanly closing

On `JSDirectStreamController`, `end` and `close` both bind to `boundDirectClose` (clean close — the arg is a close-reason, not an error), so a user `pull()` calling `controller.end(x)` cleanly finishes the upload. This PR now hands the ResumableSink itself to that same `pull()`, but here `end` maps to `jsEnd` (the same target as the new `error` alias), so `controller.end('done')` — or even `controller.end(undefined)` — reaches `write_end_request(Some(...))` → `abort_task()` and the fetch/S3 uplo

Check warning on line 516 in src/runtime/webcore/ResumableSink.rs

See this annotation in the file changed.

@claude claude / Claude Code Review

js_end/js_close clear the flushPromise slot without settling it

`js_end` and `js_close` call `detach_js()`, which zeroes the new `flushPromise` slot at line 515 without settling it — unlike `drain()` and `cancel()`, which `take_flush_promise()` and resolve/reject first. A user `type:"direct"` pull that does `write()→false; const p = controller.flush(true); controller.close(); await p` will hang on `p` forever. Mirror `cancel()`: `take_flush_promise()` + resolve (clean close) / reject (end-with-error) before `detach_js()`.

Check warning on line 1568 in src/jsc/bindings/webcore/streams/BunStreamSource.cpp

See this annotation in the file changed.

@claude claude / Claude Code Review

resumableSetupDirect: !pull.isCallable() branch returns without releasing/checking the ThrowScope

The `!pull.isCallable()` branch calls `invokeMethod(..., endPublicName, ...)` then bare-`return;`s without `scope.release()` or a trailing `RETURN_IF_EXCEPTION`, so the outer ThrowScope destructs unreleased and asserts under `BUN_JSC_validateExceptionChecks=1`. Every sibling exit in this file follows the convention (`readDirectStream`'s equivalent branch has `RETURN_IF_EXCEPTION` after the close call; `resumableSetup`'s tail is `RELEASE_AND_RETURN`) — add `scope.release();` before the `invokeMet