From 462ae8aeb8687daf465dd7ddb161f23910dc4f2c Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Mon, 22 Jun 2026 00:17:38 +0900 Subject: [PATCH] stream: proxy first own method in Readable.wrap() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The method-proxying loop in Readable.prototype.wrap() started at index 1, silently skipping streamKeys[0] and leaving a method at the first own-enumerable key unproxied. This was introduced in ee9e2a2eb6 when ArrayPrototypeForEach( ObjectKeys(stream), ...) — which iterates from index 0 — was rewritten as a manual for loop for performance. The faithful translation starts at 0; a sibling loop converted in the same commit correctly does so. The bug stayed hidden because keys[0] is usually _events (a non-function), which the loop's typeof guard skips anyway. Signed-off-by: Daijiro Wachi --- lib/internal/streams/readable.js | 2 +- ...est-stream2-readable-wrap-proxy-methods.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-stream2-readable-wrap-proxy-methods.js diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 296818e31eac09..f28e6905a9499f 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -1349,7 +1349,7 @@ Readable.prototype.wrap = function(stream) { // Proxy all the other methods. Important when wrapping filters and duplexes. const streamKeys = ObjectKeys(stream); - for (let j = 1; j < streamKeys.length; j++) { + for (let j = 0; j < streamKeys.length; j++) { const i = streamKeys[j]; if (this[i] === undefined && typeof stream[i] === 'function') { this[i] = stream[i].bind(stream); diff --git a/test/parallel/test-stream2-readable-wrap-proxy-methods.js b/test/parallel/test-stream2-readable-wrap-proxy-methods.js new file mode 100644 index 00000000000000..6ed7072c7881e9 --- /dev/null +++ b/test/parallel/test-stream2-readable-wrap-proxy-methods.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Readable } = require('stream'); + +// Readable.prototype.wrap() proxies the methods of the wrapped old-style +// stream onto the new Readable. Regression test for an off-by-one that made +// the proxy loop start at index 1, silently skipping the first own-enumerable +// key returned by Object.keys() — so a method happening to sit at that first +// position was never proxied. + +// A minimal old-style stream whose *first* own-enumerable key is a method. +// `Object.keys()` preserves insertion order for string keys, so `firstMethod` +// is `streamKeys[0]` — exactly the slot the bug skipped. +const source = { + firstMethod() { return `first:${this === source}`; }, + secondMethod() { return `second:${this === source}`; }, + on() { return this; }, + pause() {}, + resume() {}, +}; + +assert.strictEqual(Object.keys(source)[0], 'firstMethod'); + +const wrapped = new Readable().wrap(source); + +// The method at the first key must be proxied (was `undefined` before the fix). +assert.strictEqual(typeof wrapped.firstMethod, 'function'); +assert.strictEqual(typeof wrapped.secondMethod, 'function'); + +// Proxied methods stay bound to the original stream. +assert.strictEqual(wrapped.firstMethod(), 'first:true'); +assert.strictEqual(wrapped.secondMethod(), 'second:true'); + +// Existing Readable methods must not be clobbered by the proxying. +assert.strictEqual(wrapped.pause, Readable.prototype.pause); +assert.strictEqual(wrapped.resume, Readable.prototype.resume); + +wrapped.on('end', common.mustNotCall()); +wrapped.destroy();