Commit 9d8a9adfe83 for nodejs
commit 9d8a9adfe837255ef9e69e19e672f7cce4391fdb
Author: Qingyu Wang <40660121+colinaaa@users.noreply.github.com>
Date: Sat Nov 7 02:30:31 2026 +0800
stream: fix pipeTo assert when writer is released during microtask
pipeTo defers each slow-path write to a microtask so it does not run
synchronously during enqueue(). A race allows the pipe to shut down and
release its writer after the write is scheduled but before it runs. The
deferred write then hits an ERR_INTERNAL_ASSERTION because
writer[[stream]] is undefined.
Skip the deferred write when shutdown has already started.
Fixes: https://github.com/nodejs/node/issues/63732
Signed-off-by: Qingyu Wang <wangqingyu.c0l1n@bytedance.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/63733
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js
index 9ff4670e93b..97bdf261377 100644
--- a/lib/internal/webstreams/readablestream.js
+++ b/lib/internal/webstreams/readablestream.js
@@ -1792,6 +1792,7 @@ function readableStreamPipeTo(
function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
+ if (writer[kState].stream === undefined) return;
writableStreamDefaultWriterWriteWithRequest(writer, chunk, writeTracker);
pump();
}
diff --git a/test/parallel/test-webstreams-pipeto-writer-released-race.js b/test/parallel/test-webstreams-pipeto-writer-released-race.js
new file mode 100644
index 00000000000..20e0933bd60
--- /dev/null
+++ b/test/parallel/test-webstreams-pipeto-writer-released-race.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { ReadableStream, WritableStream } = require('stream/web');
+
+{
+ let sourceController;
+ let destController;
+
+ const source = new ReadableStream({
+ start(controller) {
+ sourceController = controller;
+ },
+ });
+
+ const dest = new WritableStream({
+ start(controller) {
+ destController = controller;
+ },
+ write() {},
+ });
+
+ assert.rejects(
+ source.pipeTo(dest, { preventCancel: true }),
+ { message: 'destination errored' },
+ ).then(common.mustCall());
+
+ setImmediate(common.mustCall(() => {
+ destController.error(new Error('destination errored'));
+ sourceController.enqueue('chunk');
+ }));
+}
+
+{
+ const ac = new AbortController();
+ let sourceController;
+ const chunks = [];
+
+ const source = new ReadableStream({
+ start(controller) {
+ sourceController = controller;
+ },
+ }, { highWaterMark: 0 });
+
+ const dest = new WritableStream({
+ write: common.mustCall((chunk) => {
+ chunks.push(chunk);
+ }),
+ }, { highWaterMark: 1 });
+
+ assert.rejects(
+ source.pipeTo(dest, { signal: ac.signal }),
+ { name: 'AbortError' },
+ ).then(common.mustCall(() => {
+ assert.deepStrictEqual(chunks, ['chunk']);
+ }));
+
+ setImmediate(common.mustCall(() => {
+ sourceController.enqueue('chunk');
+ ac.abort();
+ }));
+}