From 6fc970953535312364fd49a906eafef503e8a4f0 Mon Sep 17 00:00:00 2001 From: sangwook Date: Mon, 29 Dec 2025 10:18:56 +0900 Subject: [PATCH] stream: fix Writable.toWeb() hang on synchronous drain A race condition in the Writable.toWeb() adapter caused the stream to hang if the underlying Node.js Writable emitted a 'drain' event synchronously during a write() call. This often happened when highWaterMark was set to 0. By checking writableNeedDrain immediately after a backpressured write, the adapter now correctly detects if the stream has already drained, resolving the backpressure promise instead of waiting indefinitely for an event that has already occurred. Fixes: https://github.com/nodejs/node/issues/61145 --- lib/internal/webstreams/adapters.js | 3 +++ ...g-webstreams-adapters-to-writablestream.js | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 188f715a62d3c1..f19ac70eeb7982 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -238,6 +238,9 @@ function newWritableStreamFromStreamWritable(streamWritable, options = kEmptyObj options[kValidateChunk]?.(chunk); if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) { backpressurePromise = PromiseWithResolvers(); + if (!streamWritable.writableNeedDrain) { + backpressurePromise.resolve(); + } return SafePromisePrototypeFinally( backpressurePromise.promise, () => { backpressurePromise = undefined; diff --git a/test/parallel/test-whatwg-webstreams-adapters-to-writablestream.js b/test/parallel/test-whatwg-webstreams-adapters-to-writablestream.js index 23e6319563daa0..60c0538a0c1efb 100644 --- a/test/parallel/test-whatwg-webstreams-adapters-to-writablestream.js +++ b/test/parallel/test-whatwg-webstreams-adapters-to-writablestream.js @@ -197,3 +197,26 @@ class TestWritable extends Writable { assert.strictEqual(chunk, arrayBuffer); })); } + +{ + // Test that the stream doesn't hang when the underlying Writable + // emits 'drain' synchronously during write(). + // Fixes: https://github.com/nodejs/node/issues/61145 + const writable = new Writable({ + write(chunk, encoding, callback) { + callback(); + }, + }); + + // Force synchronous 'drain' emission during write() + // to simulate a stream that doesn't have Node.js's built-in kSync protection. + writable.write = function(chunk) { + this.emit('drain'); + return false; + }; + + const writableStream = newWritableStreamFromStreamWritable(writable); + const writer = writableStream.getWriter(); + writer.write(new Uint8Array([1, 2, 3])).then(common.mustCall()); + writer.write(new Uint8Array([4, 5, 6])).then(common.mustCall()); +}