From 199d95b4f651b519d75e9ec71bb49f9f8a928956 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 27 Oct 2026 14:23:06 +0100 Subject: [PATCH] stream: reject writes during WritableStream close WritableStreamDefaultControllerProcessClose clears the controller algorithms while the stream is still 'writable', before the sink close settles. A write in that window, such as writer.write() after writer.close() while the sink's close() or a TransformStream's flush() is pending, reached the cleared size algorithm and failed an internal assertion instead of being rejected as closing. Accept that state in writableStreamDefaultControllerGetChunkSize so the write is rejected with a TypeError, as it is once the stream has closed. Refs: https://github.com/nodejs/node/issues/57272 Refs: https://github.com/whatwg/streams/issues/1341 Signed-off-by: Matteo Collina --- lib/internal/webstreams/writablestream.js | 8 ++-- .../test-whatwg-writablestream-close.js | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js index 31f2a1f875dc..77e8b9cb72fe 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -1241,9 +1241,11 @@ function writableStreamDefaultControllerGetChunkSize(controller, chunk) { sizeAlgorithm, } = controller[kState]; if (sizeAlgorithm === undefined) { - assert(stream[kState].state === 'closed' || - stream[kState].state === 'errored' || - stream[kState].state === 'erroring'); + // The algorithms are also cleared while the close request is in + // flight, before the stream leaves the 'writable' state; the caller + // then rejects the write as closing. + assert(stream[kState].state !== 'writable' || + stream[kState].closeQueuedOrInFlight); return 1; } diff --git a/test/parallel/test-whatwg-writablestream-close.js b/test/parallel/test-whatwg-writablestream-close.js index 1b987cf0c414..d89340cbfcb9 100644 --- a/test/parallel/test-whatwg-writablestream-close.js +++ b/test/parallel/test-whatwg-writablestream-close.js @@ -27,3 +27,40 @@ test('should throw error when writing after close', async (t) => { } ); }); + +test('should reject writes while the close is in flight', async () => { + const { promise, resolve } = Promise.withResolvers(); + const writable = new WritableStream({ + close() { + return promise; + }, + }); + + const writer = writable.getWriter(); + const closed = writer.close(); + // Let the close request reach the sink. + await new Promise(setImmediate); + + await assert.rejects(writer.write('World'), { name: 'TypeError' }); + + resolve(); + await closed; +}); + +test('should reject writes to a TransformStream while flush() is pending', async () => { + const { promise, resolve } = Promise.withResolvers(); + const transform = new TransformStream({ + flush() { + return promise; + }, + }); + + const writer = transform.writable.getWriter(); + const closed = writer.close(); + await new Promise(setImmediate); + + await assert.rejects(writer.write('World'), { name: 'TypeError' }); + + resolve(); + await closed; +});