diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index b6294f7e5e92..4dd4c29f8670 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -978,7 +978,7 @@ impl FileSink { pub fn write(&self, data: &streams::Result) -> streams::Writable { if self.done.get() { - return streams::Writable::Done; + return streams::Writable::Owned(0); } // SAFETY(JsCell): `IOWriter::write` buffers/writes to fd; does not call JS. let rc = self.writer.with_mut(|w| w.write(data.slice())); @@ -992,7 +992,7 @@ impl FileSink { pub fn write_latin1(&self, data: &streams::Result) -> streams::Writable { if self.done.get() { - return streams::Writable::Done; + return streams::Writable::Owned(0); } // SAFETY(JsCell): `IOWriter::write_latin1` buffers/writes; no JS. let rc = self.writer.with_mut(|w| w.write_latin1(data.slice())); @@ -1001,7 +1001,7 @@ impl FileSink { pub fn write_utf16(&self, data: &streams::Result) -> streams::Writable { if self.done.get() { - return streams::Writable::Done; + return streams::Writable::Owned(0); } // SAFETY(JsCell): `IOWriter::write_utf16` buffers/writes; no JS. let rc = self.writer.with_mut(|w| w.write_utf16(data.slice16())); @@ -1017,6 +1017,7 @@ impl FileSink { // goes via the stored `*mut FileSink` backref, not this borrow. match self.writer.with_mut(|w| w.flush()) { WriteResult::Done(written) => { + self.done.set(true); self.written.set(self.written.get() + written as usize); // @truncate self.writer.with_mut(|w| w.end()); sys::Result::Ok(()) @@ -1036,6 +1037,7 @@ impl FileSink { sys::Result::Ok(()) } WriteResult::Wrote(written) => { + self.done.set(true); self.written.set(self.written.get() + written as usize); // @truncate self.writer.with_mut(|w| w.end()); sys::Result::Ok(()) @@ -1097,6 +1099,7 @@ impl FileSink { match flush_result { WriteResult::Done(written) => { + self.done.set(true); self.update_ref(false); self.writer.with_mut(|w| w.end()); sys::Result::Ok(JSValue::js_number(written as f64)) @@ -1125,6 +1128,7 @@ impl FileSink { sys::Result::Ok(unsafe { (*promise_result).to_js() }) } WriteResult::Wrote(written) => { + self.done.set(true); self.writer.with_mut(|w| w.end()); sys::Result::Ok(JSValue::js_number(written as f64)) } diff --git a/test/js/bun/util/filesink.test.ts b/test/js/bun/util/filesink.test.ts index e8a208a3a268..f9d416c9a2d1 100644 --- a/test/js/bun/util/filesink.test.ts +++ b/test/js/bun/util/filesink.test.ts @@ -309,6 +309,46 @@ it.skipIf(!isPosix)("writing after end() fails during flush does not crash", asy await 1; }); +it("write() and flush() after end() return 0 and undefined, not boolean true", async () => { + const path = join(tmpdirSync(), "filesink-write-after-end.txt"); + const writer = Bun.file(path).writer(); + + // write() may return a number synchronously (POSIX regular file) or a + // Promise (Windows regular-file uv_fs_write); await either. + expect(await writer.write("hello")).toBe(5); + await Promise.resolve(writer.end()); + + // write()/flush() are typed `number | Promise`. After end() they + // returned the boolean `true` because end_from_js() never set `done` on the + // synchronous success paths, so the call fell through to the already-ended + // IOWriter. Now they return 0 / undefined like every other sink. + expect(writer.write("world")).toBe(0); + expect(writer.flush()).toBeUndefined(); + + // The dropped write must not appear in the file. + expect(await Bun.file(path).text()).toBe("hello"); + + // Calling end() again is idempotent and must not throw. + await Promise.resolve(writer.end()).catch(() => {}); +}); + +it("write() to an exited subprocess's stdin returns 0, not boolean true", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "process.exit(0)"], + env: bunEnv, + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }); + await proc.exited; + + // The stdin FileSink's `done` is set by on_attached_process_exit. Writing + // to it returns 0 (nothing written), matching the HTTP/network sinks and + // the declared `number | Promise` type. + expect(proc.stdin.write("late\n")).toBe(0); + await Promise.resolve(proc.stdin.end()).catch(() => {}); +}); + // On Windows the libuv write completion path re-enters JS (promise resolution) // while a `&mut WindowsStreamingWriter` is live, so without raw-ptr laundering // LLVM `noalias` lets release builds cache stale `is_done`/`parent` and