From 28f4c68534912687d27401b1f2307165fa01ae5e Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Sun, 26 Jul 2026 20:59:35 +0200 Subject: [PATCH] stream: prevent enqueue after cancel in Readable.toWeb() --- lib/internal/webstreams/adapters.js | 1 + ...webstreams-adapters-cancel-backpressure.js | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 test/parallel/test-webstreams-adapters-cancel-backpressure.js diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 42e1221bb30765..edec59b3fe704c 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-webstreams-adapters-cancel-backpressure.js b/test/parallel/test-webstreams-adapters-cancel-backpressure.js new file mode 100644 index 00000000000000..25e2b82e3f17fc --- /dev/null +++ b/test/parallel/test-webstreams-adapters-cancel-backpressure.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const { PassThrough, Readable, pipeline } = require('node:stream'); +const { WritableStream } = require('node:stream/web'); +const { setTimeout } = require('node:timers/promises'); + +// Regression test for https://github.com/nodejs/node/issues/64529 +// Readable.toWeb() uncaughtException when the stream is canceled during backpressure resume + +process.on('uncaughtException', (err) => { + console.error('Uncaught exception!', err); + process.exit(1); +}); + +async function run() { + for (let i = 0; i < 50; i++) { + const src = new Readable({ + read() { + this.push(Buffer.alloc(16 * 1024, 1)); + if ((this.bytes = (this.bytes || 0) + 16384) > 512 * 1024) this.push(null); + }, + }); + const pt = new PassThrough({ highWaterMark: 16384 }); + pipeline(src, pt, () => {}); + const web = Readable.toWeb(pt); + + const ac = new AbortController(); + const writer = new WritableStream( + { + async write() { + await setTimeout(1); + } + }, + { highWaterMark: 1 }, + ); + + // Disconnect randomly to catch the exact tick window + setTimeout(i % 10).then(() => ac.abort()).then(common.mustCall()); + + try { + await web.pipeTo(writer, { signal: ac.signal }); + } catch { + // Ignore abort errors + } + + await new Promise((r) => setImmediate(r)); + } +} + +run().then(common.mustCall(() => { + process.exit(0); +}));