diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 42e1221bb30..edec59b3fe7 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -541,6 +541,7 @@ function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObj streamReadable.pause(); streamReadable.on('data', function onData(chunk) { + if (wasCanceled) return; // Copy the Buffer to detach it from the pool. if (Buffer.isBuffer(chunk) && !objectMode) chunk = new Uint8Array(chunk); diff --git a/test/parallel/test-stream-readable-to-web-termination.js b/test/parallel/test-stream-readable-to-web-termination.js index f30cf721e14..8a15fa3f5fd 100644 --- a/test/parallel/test-stream-readable-to-web-termination.js +++ b/test/parallel/test-stream-readable-to-web-termination.js @@ -42,3 +42,25 @@ const { setTimeout: delay } = require('timers/promises'); assert.deepStrictEqual(closeResult, { value: undefined, done: true }); })().then(common.mustCall()); } + +// Cancelling a web ReadableStream while the underlying Readable is actively +// producing data should not throw ERR_INVALID_STATE. The 'data' handler in +// newReadableStreamFromStreamReadable must check wasCanceled before calling +// controller.enqueue(). See: https://github.com/nodejs/node/issues/54205 +{ + const readable = new Readable({ + read() { + this.push(Buffer.alloc(1024)); + }, + }); + + const webStream = Readable.toWeb(readable); + const reader = webStream.getReader(); + + (async () => { + await reader.read(); + await reader.read(); + reader.releaseLock(); + await webStream.cancel(); + })().then(common.mustCall()); +}