From c457f364bc8077b432dc809b375685052eb21b3d Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Fri, 15 May 2026 15:27:05 -0700 Subject: [PATCH 1/2] stream: fix merge handling for object-like sources merge() treated any final non-iterable object as an options object. That dropped valid from() inputs such as ArrayBuffer, ArrayBufferView, and streamable protocol objects. Fixes: https://github.com/nodejs/node/issues/63355 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 --- lib/internal/streams/iter/consumers.js | 8 +++++++ .../test-stream-iter-consumers-merge.js | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js index 442fe95b8e1b85..7fb685956d4de5 100644 --- a/lib/internal/streams/iter/consumers.js +++ b/lib/internal/streams/iter/consumers.js @@ -8,6 +8,7 @@ // ondrain() - backpressure drain utility const { + ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, ArrayBufferPrototypeSlice, ArrayPrototypeMap, @@ -51,9 +52,12 @@ const { const { drainableProtocol, + toAsyncStreamable, + toStreamable, } = require('internal/streams/iter/types'); const { + isAnyArrayBuffer, isSharedArrayBuffer, } = require('internal/util/types'); @@ -65,6 +69,10 @@ function isMergeOptions(value) { return ( value !== null && typeof value === 'object' && + !isAnyArrayBuffer(value) && + !ArrayBufferIsView(value) && + typeof value[toStreamable] !== 'function' && + typeof value[toAsyncStreamable] !== 'function' && !isAsyncIterable(value) && !isSyncIterable(value) ); diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js index ad047c0ffd7ed4..c5b18be042d874 100644 --- a/test/parallel/test-stream-iter-consumers-merge.js +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -9,6 +9,8 @@ const { push, merge, text, + toAsyncStreamable, + toStreamable, } = require('stream/iter'); // ============================================================================= @@ -162,6 +164,27 @@ async function testMergeStringSources() { assert.ok(combined.includes('world')); } +// merge() accepts object-like sources that are normalized via from() +async function testMergeObjectLikeSources() { + const arrayBuffer = new TextEncoder().encode('abc').buffer; + const dataView = new DataView(new TextEncoder().encode('def').buffer); + const streamable = { + [toStreamable]() { + return 'ghi'; + }, + }; + const asyncStreamable = { + [toAsyncStreamable]() { + return Promise.resolve('jkl'); + }, + }; + + assert.strictEqual(await text(merge(arrayBuffer)), 'abc'); + assert.strictEqual(await text(merge(dataView)), 'def'); + assert.strictEqual(await text(merge(streamable)), 'ghi'); + assert.strictEqual(await text(merge(asyncStreamable)), 'jkl'); +} + Promise.all([ testMergeTwoSources(), testMergeSingleSource(), @@ -172,4 +195,5 @@ Promise.all([ testMergeConsumerBreak(), testMergeSignalMidIteration(), testMergeStringSources(), + testMergeObjectLikeSources(), ]).then(common.mustCall()); From 2502bfea228f7199b113a9d4558eb40e1b429a5c Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Fri, 15 May 2026 17:35:54 -0700 Subject: [PATCH 2/2] stream: check ArrayBuffer last in iter merge options --- lib/internal/streams/iter/consumers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js index 7fb685956d4de5..bcfe5d5ab29749 100644 --- a/lib/internal/streams/iter/consumers.js +++ b/lib/internal/streams/iter/consumers.js @@ -69,12 +69,12 @@ function isMergeOptions(value) { return ( value !== null && typeof value === 'object' && - !isAnyArrayBuffer(value) && !ArrayBufferIsView(value) && typeof value[toStreamable] !== 'function' && typeof value[toAsyncStreamable] !== 'function' && !isAsyncIterable(value) && - !isSyncIterable(value) + !isSyncIterable(value) && + !isAnyArrayBuffer(value) ); }