Conversation
Walkthrough
ChangesFileSink write-after-end enforcement
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 5:05 AM PT - Jun 28th, 2026
❌ @robobun, your commit a7960e3 has some failures in 🧪 To try this PR locally: bunx bun-pr 32857That installs a local version of the PR into your bun-32857 --bun |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/js/internal/fs/streams.ts`:
- Around line 674-705: The fast-path in writeFast() is bypassing
Writable.prototype.write(), so fast write-after-end failures need to follow the
same normal Writable error handling as src/js/internal/streams/writable.ts.
Update the promise rejection and synchronous throw branches in writeFast() to
route errors through the shared error helper used by Writable (instead of
calling cb(err) and conditionally destroy()), so callback-based writes after
end() still emit the standard error event and mark the stream errored
consistently.
In `@src/runtime/webcore/Sink.rs`:
- Around line 875-879: The write-after-end path in Sink::write_after_end_error
is overriding the canonical ERR_STREAM_WRITE_AFTER_END text with a sink-specific
sentence; update the throw message to use the standard “write after end” payload
expected by ErrorCode.cpp and stream tests. Keep the existing error flow in
Sink.rs, but replace the formatted T::NAME-specific wording so the emitted error
matches the canonical stream error exactly.
In `@test/js/bun/util/filesink.test.ts`:
- Around line 374-378: The test currently pulls in child_process with a runtime
require inside the test body, which should be moved to a module-scope import for
consistency with repo test style. Update the filesink.test.ts fixture so
child_process is imported at the top level and the test case uses that imported
symbol when calling spawn, keeping the test focused on the bunExe() behavior
rather than dynamic require().
- Around line 312-314: The new filesink test currently creates its temporary
path with tmpdirSync, which bypasses the harness cleanup flow. Update the test
in filesink.write-after-end to use tempDir from harness with using dir so the
directory is cleaned up automatically, and keep the rest of the test setup the
same around Bun.file(...).writer().
- Around line 333-336: The post-end assertions in filesink tests are too weak
for the new contract. Update the relevant checks around writer.flush() after
end() and the peer-close write() cases to assert the exact return values from
the existing test helpers, using the concrete symbols involved in these cases
rather than broad negated/type checks. Make the expectations verify undefined
for flush() after end and 0 for peer-close write(), and apply the same stronger
assertions in the other mentioned test block as well.
- Around line 380-399: In the file sink test around the p.stdin close/write
flow, the waiters closed and cbFired can hang because only the success paths are
wired. Update the Promise.withResolvers setup so the p.stdin.on("error"),
p.on("error"), and p.on("exit") handlers reject both the closed and callback
waiters on any failure path. Keep the existing p.stdin.on("close") and write
callback/sync-throw assertions, but ensure all child/process/stdin error cases
propagate immediately instead of timing out.
🪄 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: 7e7d51a4-7d45-4fb2-9336-6b8641687de9
📒 Files selected for processing (4)
src/js/internal/fs/streams.tssrc/runtime/webcore/FileSink.rssrc/runtime/webcore/Sink.rstest/js/bun/util/filesink.test.ts
|
CI status: no test failures. On #66207 (the current head,
The only
|
Jarred-Sumner
left a comment
There was a problem hiding this comment.
Changing this in FileSink is an unnecessary breaking change. Don't do that.
FileSink.write() after a successful end() returned the boolean true, against the declared number | Promise<number> type, and flush() returned true instead of undefined. Cause: end() and end_from_js() only set self.done on the Err and Pending flush results. On the synchronous Done/Wrote paths (the common case for a regular file) they called writer.end() on the IOWriter but left self.done false, so a subsequent write() fell through to the already-ended IOWriter, which returned WriteResult::Done(0), mapped to Writable::Done, converted to JSValue::TRUE. Fix: set self.done on every terminal branch of end()/end_from_js(), and make write()/write_latin1()/write_utf16() return Writable::Owned(0) instead of Writable::Done when the sink is done, matching the HTTP/network sinks. No new exceptions are thrown and data is still silently ignored after end(); only the return values change to match the documented types.
ee8742c to
a7960e3
Compare
|
@Jarred-Sumner Understood, removed. I force-pushed the branch down to a single commit ( What is left is just the return-type mismatch I hit along the way: If that is also more than you want to change, say so and I'll close this. |
|
The same behavior was reported again independently (write to I split out the part that does not touch FileSink into #33511: This PR is still just the |
Repro
FileSink.write()andflush()afterend()return the booleantrue, against the declarednumber | Promise<number>return type.Cause
FileSink::end()/end_from_js()only setself.done = trueon theErrandPendingflush results. On the synchronousDone/Wrotebranches (the common case for a regular file) they callwriter.end()on the underlyingIOWriterbut leaveself.donefalse.A subsequent
write()then skips thedoneearly-return and callswriter.write()on the already-endedIOWriter, which returnsWriteResult::Done(0).to_result(Done(0))→Writable::Done→JSValue::TRUE.Fix
self.done = trueon every terminal branch ofFileSink::end()andend_from_js().write()/write_latin1()/write_utf16()returnWritable::Owned(0)instead ofWritable::Donewhen the sink is done.Owned(0)converts to the number0, which is what the HTTP/H3/network sinks already return in the same situation.With
donenow set,flush()afterend()hits the existingself.doneguard inflush_from_jsand returnsundefinedinstead oftrue.No new exceptions are thrown and post-end writes are still silently ignored. Only the return values change, to match the documented
number | Promise<number>type and the other sinks. (An earlier revision of this PR made write-after-end throwERR_STREAM_WRITE_AFTER_END; that was removed per review as an unnecessary breaking change.)Verification
New tests in
test/js/bun/util/filesink.test.ts:write()afterend()returns0andflush()returnsundefined; the post-end bytes do not appear in the file.proc.stdin.write()after the subprocess exits returns0.Both fail on 1.4.0 (get
true).Related: the
Bun.servestreaming-body-error0\r\n\r\nterminator half of the same fuzzer finding is handled in #32842.