diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 8befa6bbbafd72..06418853292928 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -507,6 +507,7 @@ function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObj let wasCanceled = false; 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 13fce9bc715e1e..df18d95d57c3ee 100644 --- a/test/parallel/test-stream-readable-to-web-termination.js +++ b/test/parallel/test-stream-readable-to-web-termination.js @@ -10,3 +10,25 @@ const { Readable } = require('stream'); const reader = Readable.toWeb(r).getReader(); reader.read(); } + +// Cancelling a web ReadableStream while the underlying Readable is actively +// producing data should not throw ERR_INVALID_STATE. The onData 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(); + })(); +}