diff --git a/src/runtime/webcore/Sink.rs b/src/runtime/webcore/Sink.rs index b7e1127eeac0..d84639a287a9 100644 --- a/src/runtime/webcore/Sink.rs +++ b/src/runtime/webcore/Sink.rs @@ -209,6 +209,22 @@ impl JSSink { std::ptr::from_mut::(ptr).cast::(), (&raw mut bits).cast::<*mut c_void>(), ); + // `${name}__assignToStream` creates the JSReadable*SinkController with + // m_sinkPtr=ptr before calling into the stream pump. If the pump setup + // throws (e.g. a direct stream's `pull` getter), nothing ever calls + // end()/close() on the controller, so its destructor would run + // `${name}__finalize(m_sinkPtr)` after the caller has freed the sink. + // Detach it now while `ptr` is still live; the controller's later GC + // then sees m_sinkPtr==null and skips the native finalize. + if bits != 0 && result.to_error().is_some() { + if let Some(src) = ptr.source() { + *src = streams::SourceHandle::None; + } + let _ = ::bun_jsc::call_check_slow(global, || { + streams::controller_abi::detach_ptr(JSValue::from_encoded(bits)) + }); + return result; + } if let Some(src) = ptr.source() { if matches!(*src, streams::SourceHandle::JSController(_)) { *src = if bits != 0 { diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 46ec5a28bc28..5b6da5beda1c 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -1252,6 +1252,7 @@ it.skipIf(isWindows)("leaves a caller-supplied stdout fd open when stdin stream fstatSync(fd); writeSync(fd, "still-open"); closeSync(fd); + Bun.gc(true); console.log(message); process.exit(0); `; @@ -1261,6 +1262,7 @@ it.skipIf(isWindows)("leaves a caller-supplied stdout fd open when stdin stream stdio: ["ignore", "pipe", "pipe"], }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); expect(stdout.trim()).toBe("pull unavailable"); expect(readFileSync(file, "utf8")).toContain("still-open"); expect(exitCode).toBe(0); @@ -1293,6 +1295,7 @@ it.skipIf(isWindows)("leaves a Bun.file(fd) stdout open when stdin stream setup fstatSync(fd); writeSync(fd, "still-open"); closeSync(fd); + Bun.gc(true); console.log(message); process.exit(0); `;