From f7d7d13d607a4c6acc2e55f97274ef79be4649d1 Mon Sep 17 00:00:00 2001 From: aelhor Date: Wed, 17 Dec 2025 19:15:53 +0200 Subject: [PATCH 1/2] streams: forward errors correctly for duplexPair endpoints fix the duplexPair implementation so that when one side is destroyed with an error, the other side also receives the error or a close event as appropriate. previous behavior caused sideA to never emit an 'error' or 'close' when sideB errored, which prevented users from observing or handling the paired stream failure. Fixes: https://github.com/nodejs/node/issues/61015 --- lib/internal/streams/duplexpair.js | 18 +++++++++++++- test/parallel/test-duplex-error.js | 39 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-duplex-error.js diff --git a/lib/internal/streams/duplexpair.js b/lib/internal/streams/duplexpair.js index a32084c4d4cbdf..3265e9f797da20 100644 --- a/lib/internal/streams/duplexpair.js +++ b/lib/internal/streams/duplexpair.js @@ -50,6 +50,22 @@ class DuplexSide extends Duplex { this.#otherSide.on('end', callback); this.#otherSide.push(null); } + + + _destroy(err, callback) { + + if (err) { + // Error case: tell the other side to also destroy with that error. + this.#otherSide.destroy(err); + } else { + // Graceful close case (destroy() without error): + // send an EOF to the other side's readable end if it hasn't already closed. + if (this.#otherSide && !this.#otherSide.destroyed) { + this.#otherSide.push(null); + } + } + callback(err); + } } function duplexPair(options) { @@ -57,6 +73,6 @@ function duplexPair(options) { const side1 = new DuplexSide(options); side0[kInitOtherSide](side1); side1[kInitOtherSide](side0); - return [ side0, side1 ]; + return [side0, side1]; } module.exports = duplexPair; diff --git a/test/parallel/test-duplex-error.js b/test/parallel/test-duplex-error.js new file mode 100644 index 00000000000000..d3e73f010ca754 --- /dev/null +++ b/test/parallel/test-duplex-error.js @@ -0,0 +1,39 @@ +'use strict'; + +const assert = require('assert'); +const { duplexPair } = require('stream'); + +const [sideA, sideB] = duplexPair(); + +let sideAErrorReceived = false; +let sideBErrorReceived = false; + +// Add error handlers +sideA.on('error', (err) => { + sideAErrorReceived = true; +}); +sideB.on('error', (err) => { + sideBErrorReceived = true; +}); + +// Ensure the streams are flowing +sideA.resume(); +sideB.resume(); + +// Destroy sideB with an error +sideB.destroy(new Error('Simulated error')); + +// Wait for event loop to process error events +setImmediate(() => { + assert.strictEqual( + sideAErrorReceived, + true, + 'sideA should receive the error when sideB is destroyed with an error' + ); + assert.strictEqual( + sideBErrorReceived, + true, + 'sideB should emit its own error when destroyed' + ); +}); + From 45bc784214cc4ce737527419049108ceec16f99f Mon Sep 17 00:00:00 2001 From: aelhor Date: Thu, 18 Dec 2025 21:11:09 +0200 Subject: [PATCH 2/2] lint: fix style issues in duplexpair and its tests --- lib/internal/streams/duplexpair.js | 7 ++----- test/parallel/test-duplex-error.js | 33 +++++++++++------------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/lib/internal/streams/duplexpair.js b/lib/internal/streams/duplexpair.js index 3265e9f797da20..daeacc4ac9c42d 100644 --- a/lib/internal/streams/duplexpair.js +++ b/lib/internal/streams/duplexpair.js @@ -53,16 +53,13 @@ class DuplexSide extends Duplex { _destroy(err, callback) { - if (err) { // Error case: tell the other side to also destroy with that error. this.#otherSide.destroy(err); - } else { + } else if (this.#otherSide && !this.#otherSide.destroyed) { // Graceful close case (destroy() without error): // send an EOF to the other side's readable end if it hasn't already closed. - if (this.#otherSide && !this.#otherSide.destroyed) { - this.#otherSide.push(null); - } + this.#otherSide.push(null); } callback(err); } diff --git a/test/parallel/test-duplex-error.js b/test/parallel/test-duplex-error.js index d3e73f010ca754..bdc2912f38bc0e 100644 --- a/test/parallel/test-duplex-error.js +++ b/test/parallel/test-duplex-error.js @@ -1,5 +1,6 @@ 'use strict'; +const common = require('../common'); const assert = require('assert'); const { duplexPair } = require('stream'); @@ -8,32 +9,22 @@ const [sideA, sideB] = duplexPair(); let sideAErrorReceived = false; let sideBErrorReceived = false; -// Add error handlers -sideA.on('error', (err) => { +// Use common.mustCall inside the listeners to ensure they trigger +sideA.on('error', common.mustCall((err) => { sideAErrorReceived = true; -}); -sideB.on('error', (err) => { +})); + +sideB.on('error', common.mustCall((err) => { sideBErrorReceived = true; -}); +})); -// Ensure the streams are flowing sideA.resume(); sideB.resume(); -// Destroy sideB with an error sideB.destroy(new Error('Simulated error')); -// Wait for event loop to process error events -setImmediate(() => { - assert.strictEqual( - sideAErrorReceived, - true, - 'sideA should receive the error when sideB is destroyed with an error' - ); - assert.strictEqual( - sideBErrorReceived, - true, - 'sideB should emit its own error when destroyed' - ); -}); - +// Wrap the callback in common.mustCall() +setImmediate(common.mustCall(() => { + assert.strictEqual(sideAErrorReceived, true); + assert.strictEqual(sideBErrorReceived, true); +}));