From 2449435053b8eb4ab9d68ce76cddb1994cee74af Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Sat, 28 Feb 2026 14:09:57 +0100 Subject: [PATCH 1/4] stream: fix TransformStream race on cancel with pending write Signed-off-by: marcopiraccini --- lib/internal/webstreams/transformstream.js | 8 ++- ...hatwg-transformstream-cancel-write-race.js | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-whatwg-transformstream-cancel-write-race.js diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js index 371b597dc681d6..fa5e5803c159de 100644 --- a/lib/internal/webstreams/transformstream.js +++ b/lib/internal/webstreams/transformstream.js @@ -72,7 +72,6 @@ const { const assert = require('internal/assert'); const kSkipThrow = Symbol('kSkipThrow'); - const getNonWritablePropertyDescriptor = (value) => { return { __proto__: null, @@ -524,7 +523,12 @@ function transformStreamDefaultControllerError(controller, error) { async function transformStreamDefaultControllerPerformTransform(controller, chunk) { try { - return await controller[kState].transformAlgorithm(chunk, controller); + const transformAlgorithm = controller[kState].transformAlgorithm; + if (typeof transformAlgorithm !== 'function') { + // Algorithms were cleared by a concurrent cancel/abort/close. + return; + } + return await transformAlgorithm(chunk, controller); } catch (error) { transformStreamError(controller[kState].stream, error); throw error; diff --git a/test/parallel/test-whatwg-transformstream-cancel-write-race.js b/test/parallel/test-whatwg-transformstream-cancel-write-race.js new file mode 100644 index 00000000000000..eafd9aaacf99ed --- /dev/null +++ b/test/parallel/test-whatwg-transformstream-cancel-write-race.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const { TransformStream } = require('stream/web'); +const { setTimeout } = require('timers/promises'); + +// Test for https://github.com/nodejs/node/issues/62036 +// A late write racing with reader.cancel() should not throw an internal "transformAlgorithm is not a function" TypeError. + +async function test() { + const stream = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk); + }, + }); + + await setTimeout(0); + + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + + // Release backpressure. + const pendingRead = reader.read(); + + // Simulate client disconnect / shutdown. + const pendingCancel = reader.cancel(new Error('client disconnected')); + + // Late write racing with cancel. + const pendingLateWrite = writer.write('late-write'); + + const results = await Promise.allSettled([ + pendingRead, + pendingCancel, + pendingLateWrite, + ]); + + // pendingRead should fulfill (with done:true or a value). + // pendingCancel should fulfill. + // pendingLateWrite may reject, but must NOT reject with an internal + // TypeError about transformAlgorithm not being a function. + for (const result of results) { + if (result.status === 'rejected') { + const err = result.reason; + if (err instanceof TypeError && + /transformAlgorithm is not a function/.test(err.message)) { + throw new Error( + 'Internal implementation error leaked: ' + err.message + ); + } + } + } +} + +test().then(common.mustCall()); From 398e70014ef69e06e60870985b876c983f956d27 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Sat, 28 Feb 2026 14:23:30 +0100 Subject: [PATCH 2/4] linting fixup Signed-off-by: marcopiraccini --- test/parallel/test-whatwg-transformstream-cancel-write-race.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-whatwg-transformstream-cancel-write-race.js b/test/parallel/test-whatwg-transformstream-cancel-write-race.js index eafd9aaacf99ed..49dcbf8fbfcdc5 100644 --- a/test/parallel/test-whatwg-transformstream-cancel-write-race.js +++ b/test/parallel/test-whatwg-transformstream-cancel-write-race.js @@ -5,7 +5,8 @@ const { TransformStream } = require('stream/web'); const { setTimeout } = require('timers/promises'); // Test for https://github.com/nodejs/node/issues/62036 -// A late write racing with reader.cancel() should not throw an internal "transformAlgorithm is not a function" TypeError. +// A late write racing with reader.cancel() should not throw an +// internal "transformAlgorithm is not a function" TypeError. async function test() { const stream = new TransformStream({ From a84a5066ceb6a6f8ca357f498275fb260040a10c Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Tue, 3 Mar 2026 05:53:51 +0100 Subject: [PATCH 3/4] test-fixup, use assert Signed-off-by: marcopiraccini --- ...hatwg-transformstream-cancel-write-race.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test/parallel/test-whatwg-transformstream-cancel-write-race.js b/test/parallel/test-whatwg-transformstream-cancel-write-race.js index 49dcbf8fbfcdc5..781d4cb1bb703d 100644 --- a/test/parallel/test-whatwg-transformstream-cancel-write-race.js +++ b/test/parallel/test-whatwg-transformstream-cancel-write-race.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common'); +const assert = require('assert'); const { TransformStream } = require('stream/web'); const { setTimeout } = require('timers/promises'); @@ -29,26 +30,24 @@ async function test() { // Late write racing with cancel. const pendingLateWrite = writer.write('late-write'); - const results = await Promise.allSettled([ + const [ + readResult, + cancelResult, + lateWriteResult, + ] = await Promise.allSettled([ pendingRead, pendingCancel, pendingLateWrite, ]); - // pendingRead should fulfill (with done:true or a value). - // pendingCancel should fulfill. - // pendingLateWrite may reject, but must NOT reject with an internal - // TypeError about transformAlgorithm not being a function. - for (const result of results) { - if (result.status === 'rejected') { - const err = result.reason; - if (err instanceof TypeError && - /transformAlgorithm is not a function/.test(err.message)) { - throw new Error( - 'Internal implementation error leaked: ' + err.message - ); - } - } + assert.strictEqual(readResult.status, 'fulfilled'); + assert.strictEqual(cancelResult.status, 'fulfilled'); + if (lateWriteResult.status === 'rejected') { + const err = lateWriteResult.reason; + const isNotAFunction = err instanceof TypeError && + /transformAlgorithm is not a function/.test(err.message); + assert.ok(!isNotAFunction, + `Internal implementation error leaked: ${err.message}`); } } From 23c602d96d8d253410112fb74b2716266bfec250 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Tue, 3 Mar 2026 15:52:36 +0100 Subject: [PATCH 4/4] test if undefined instead of typeof. use node:test and assert Signed-off-by: marcopiraccini --- lib/internal/webstreams/transformstream.js | 2 +- ...st-whatwg-transformstream-cancel-write-race.js | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js index fa5e5803c159de..e2d15c05bca249 100644 --- a/lib/internal/webstreams/transformstream.js +++ b/lib/internal/webstreams/transformstream.js @@ -524,7 +524,7 @@ function transformStreamDefaultControllerError(controller, error) { async function transformStreamDefaultControllerPerformTransform(controller, chunk) { try { const transformAlgorithm = controller[kState].transformAlgorithm; - if (typeof transformAlgorithm !== 'function') { + if (transformAlgorithm === undefined) { // Algorithms were cleared by a concurrent cancel/abort/close. return; } diff --git a/test/parallel/test-whatwg-transformstream-cancel-write-race.js b/test/parallel/test-whatwg-transformstream-cancel-write-race.js index 781d4cb1bb703d..5c32a25b9246f9 100644 --- a/test/parallel/test-whatwg-transformstream-cancel-write-race.js +++ b/test/parallel/test-whatwg-transformstream-cancel-write-race.js @@ -1,15 +1,14 @@ 'use strict'; -const common = require('../common'); -const assert = require('assert'); +require('../common'); +const { test } = require('node:test'); +const assert = require('node:assert'); const { TransformStream } = require('stream/web'); const { setTimeout } = require('timers/promises'); -// Test for https://github.com/nodejs/node/issues/62036 -// A late write racing with reader.cancel() should not throw an -// internal "transformAlgorithm is not a function" TypeError. +// https://github.com/nodejs/node/issues/62036 -async function test() { +test('Late write racing with reader.cancel() should not throw an internal TypeError', async () => { const stream = new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk); @@ -49,6 +48,4 @@ async function test() { assert.ok(!isNotAFunction, `Internal implementation error leaked: ${err.message}`); } -} - -test().then(common.mustCall()); +});