diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 847b3837af4..fa6ed790083 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -1522,8 +1522,9 @@ function createAsyncIterator(stream, options) { const chunk = stream.destroyed ? null : stream.read(); if (chunk !== null) { // Read `then` only once so that a getter cannot observe (or throw - // on) a second access. - const then = chunk.then; + // on) a second access. `undefined` is a valid chunk value, so it must + // not be dereferenced here. + const then = chunk?.then; if (typeof then === 'function') { FunctionPrototypeCall(then, chunk, (value) => { inFlight = false; @@ -1595,8 +1596,9 @@ function createAsyncIterator(stream, options) { const chunk = stream.destroyed ? null : stream.read(); if (chunk !== null) { // Read `then` only once so that a getter cannot observe (or - // throw on) a second access. - const then = chunk.then; + // throw on) a second access. `undefined` is a valid chunk value, + // so it must not be dereferenced here. + const then = chunk?.then; if (typeof then === 'function') { inFlight = true; return FunctionPrototypeCall( diff --git a/test/parallel/test-stream-readable-async-iterators.js b/test/parallel/test-stream-readable-async-iterators.js index f9bcaea6057..fb4b052bf28 100644 --- a/test/parallel/test-stream-readable-async-iterators.js +++ b/test/parallel/test-stream-readable-async-iterators.js @@ -989,5 +989,43 @@ async function tests() { })().then(common.mustCall()); } +{ + // An `undefined` chunk is a value, not end-of-stream. Here it is already + // buffered, so it is read on the synchronous fast path. + (async () => { + const r = new Readable({ objectMode: true, read() {} }); + r.push(undefined); + r.push(null); + + const it = r[Symbol.asyncIterator](); + assert.deepStrictEqual(await it.next(), { done: false, value: undefined }); + assert.strictEqual((await it.next()).done, true); + })().then(common.mustCall()); +} + +{ + // An `undefined` chunk pushed after next() is delivered once it arrives. + (async () => { + const r = new Readable({ objectMode: true, read() {} }); + const it = r[Symbol.asyncIterator](); + const next = it.next(); + setImmediate(() => { + r.push(undefined); + r.push(null); + }); + + assert.deepStrictEqual(await next, { done: false, value: undefined }); + assert.strictEqual((await it.next()).done, true); + })().then(common.mustCall()); +} + +{ + // Readable.from() delivers `undefined` values. + (async () => { + assert.deepStrictEqual(await Readable.from([undefined]).toArray(), + [undefined]); + })().then(common.mustCall()); +} + // To avoid missing some tests if a promise does not resolve tests().then(common.mustCall());