Skip to content

Commit 74bd0cd

Browse files
trivikraduh95
authored andcommitted
stream: reject pending reads on iterator throw
Settle reads that are already waiting when a push stream iterator is returned or thrown. This prevents next() promises from hanging after consumer cancellation. Fixes: #63554 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #63555 Backport-PR-URL: #64675 Fixes: #63554 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 45780df commit 74bd0cd

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

lib/internal/streams/iter/push.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ class PushQueue {
426426
if (this.#consumerState !== 'active') return;
427427
this.#consumerState = 'returned';
428428
this.#cleanup();
429+
this.#resolvePendingReads();
429430
this.#rejectPendingWrites(
430431
new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
431432
// If closing, reject the pending end promise
@@ -443,7 +444,12 @@ class PushQueue {
443444
this.#consumerState = 'thrown';
444445
this.#error = error;
445446
this.#cleanup();
447+
this.#rejectPendingReads(error);
446448
this.#rejectPendingWrites(error);
449+
if (this.#writerState === 'closing' && this.#pendingEnd) {
450+
this.#pendingEnd.reject(error);
451+
this.#pendingEnd = null;
452+
}
447453
// Reject pending drains - the consumer errored
448454
this.#rejectPendingDrains(error);
449455
}
@@ -485,6 +491,9 @@ class PushQueue {
485491
} else if (this.#writerState === 'errored' && this.#error) {
486492
const pending = this.#pendingReads.shift();
487493
pending.reject(this.#error);
494+
} else if (this.#consumerState === 'returned') {
495+
const pending = this.#pendingReads.shift();
496+
pending.resolve({ __proto__: null, value: undefined, done: true });
488497
} else {
489498
break;
490499
}

test/parallel/test-stream-iter-push-writer.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,44 @@ async function testFailRejectsPendingRead() {
326326
);
327327
}
328328

329+
// iterator.return() resolves a pending read with done:true
330+
async function testConsumerReturnResolvesPendingRead() {
331+
const { readable } = push();
332+
333+
const iter = readable[Symbol.asyncIterator]();
334+
const readPromise = iter.next();
335+
336+
await new Promise(setImmediate);
337+
338+
const returnResult = await iter.return();
339+
assert.strictEqual(returnResult.value, undefined);
340+
assert.strictEqual(returnResult.done, true);
341+
342+
const readResult = await readPromise;
343+
assert.strictEqual(readResult.value, undefined);
344+
assert.strictEqual(readResult.done, true);
345+
}
346+
347+
// iterator.throw() rejects a pending read with the thrown error
348+
async function testConsumerThrowRejectsPendingRead() {
349+
const { readable } = push();
350+
351+
const iter = readable[Symbol.asyncIterator]();
352+
const readPromise = iter.next();
353+
354+
await new Promise(setImmediate);
355+
356+
const err = new Error('consumer read boom');
357+
const throwResult = await iter.throw(err);
358+
assert.strictEqual(throwResult.value, undefined);
359+
assert.strictEqual(throwResult.done, true);
360+
361+
await assert.rejects(
362+
() => readPromise,
363+
(e) => e === err,
364+
);
365+
}
366+
329367
// end() while writes are pending rejects those writes
330368
async function testEndRejectsPendingWrites() {
331369
const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' });
@@ -438,6 +476,8 @@ Promise.all([
438476
testConsumerThrowRejectsWrites(),
439477
testEndResolvesPendingRead(),
440478
testFailRejectsPendingRead(),
479+
testConsumerReturnResolvesPendingRead(),
480+
testConsumerThrowRejectsPendingRead(),
441481
testEndRejectsPendingWrites(),
442482
testEndIdempotentWhenClosed(),
443483
testEndRejectsWhenErrored(),

0 commit comments

Comments
 (0)