From 6b47004fd67417997dc559e1dca7ecb56f2ae73c Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Fri, 25 Sep 2026 09:57:09 +0700 Subject: [PATCH] test(node): drop Readable.toWeb crash tests now fixed in Node 26.10 Node 26.10 fixed the uncatchable ERR_INVALID_STATE crash when a Readable.toWeb stream is cancelled mid-upload (nodejs/node#62773), so the tests asserting that the bare adapter crashes started failing on Node 26. Remove them and note the upstream fix on toWebReadableStream, which stays until every supported Node release has it. --- packages/node/src/utils.test.ts | 38 +++++++-------------------------- packages/node/src/utils.ts | 3 +++ 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/node/src/utils.test.ts b/packages/node/src/utils.test.ts index 7beb033..850023f 100644 --- a/packages/node/src/utils.test.ts +++ b/packages/node/src/utils.test.ts @@ -323,7 +323,7 @@ describe('toWebReadableStream', () => { /** * Below the 256 KiB flood chunk, so the consumer cancels on its first read * while the request is still streaming — the condition that crashes a bare - * `Readable.toWeb`. + * `Readable.toWeb` before Node 26.10. */ const LIMIT = 64 * 1024 @@ -437,21 +437,19 @@ describe('toWebReadableStream', () => { } /** - * Boots a server that spools each request body via `wrap`, fires `iterations` - * aborted uploads at it, and reports how many completed plus any crashes. - * `stopOnCrash` ends the flood at the first crash. + * Boots a server that spools each request body via `toWebReadableStream`, + * fires `iterations` aborted uploads at it, and reports how many completed + * plus any crashes. */ async function runUploadServer( kind: 'http1' | 'http2', - wrap: (req: Readable) => ReadableStream, iterations: number, - stopOnCrash = false, ): Promise<{ handled: number, crashes: Error[] }> { const tmpDir = await mkdtemp(path.join(tmpdir(), `toweb-${kind}-`)) let handled = 0 const listener = async (req: any, res: any): Promise => { - await spoolUntilRejected(wrap(req), tmpDir) + await spoolUntilRejected(toWebReadableStream(req), tmpDir) handled++ try { if (!res.headersSent) { @@ -467,13 +465,10 @@ describe('toWebReadableStream', () => { const server = kind === 'http1' ? createServer(listener) : createHttp2Server(listener) const flood = kind === 'http1' ? floodAndAbortHttp1 : floodAndAbortHttp2 - const crashes = await recordUncaught(async (crashes) => { + const crashes = await recordUncaught(async () => { await new Promise(resolve => server.listen(0, resolve)) const { port } = server.address() as AddressInfo for (let i = 0; i < iterations; i++) { - if (stopOnCrash && crashes.length) { - break - } await flood(port) await new Promise(resolve => setTimeout(resolve, 5)) } @@ -484,9 +479,6 @@ describe('toWebReadableStream', () => { return { handled, crashes } } - const bare = (req: Readable): ReadableStream => Readable.toWeb(req) as ReadableStream - const wrapped = (req: Readable): ReadableStream => toWebReadableStream(req) - it('converts a raw buffer stream and preserves its bytes as copies', async () => { const chunks = [Buffer.from('hello '), Buffer.from('world'), Buffer.alloc(1024, 7)] const source = Readable.from(chunks) @@ -524,29 +516,15 @@ describe('toWebReadableStream', () => { expect(source.destroyed).toBe(true) // cancellation still tears the source down }) - it('lets a bare Readable.toWeb crash an aborted HTTP/1 upload (documents the bug)', async () => { - const { crashes } = await runUploadServer('http1', bare, 25, true) - - expect(crashes.length).toBeGreaterThan(0) - expect(crashes[0]).toMatchObject({ code: 'ERR_INVALID_STATE' }) - }, 30_000) - it('keeps an aborted HTTP/1 upload from crashing the process', async () => { - const { handled, crashes } = await runUploadServer('http1', wrapped, 25) + const { handled, crashes } = await runUploadServer('http1', 25) expect(crashes).toEqual([]) expect(handled).toBe(25) }, 30_000) - it('lets a bare Readable.toWeb crash an aborted HTTP/2 upload (documents the bug)', async () => { - const { crashes } = await runUploadServer('http2', bare, 25, true) - - expect(crashes.length).toBeGreaterThan(0) - expect(crashes[0]).toMatchObject({ code: 'ERR_INVALID_STATE' }) - }, 30_000) - it('keeps an aborted HTTP/2 upload from crashing the process', async () => { - const { handled, crashes } = await runUploadServer('http2', wrapped, 25) + const { handled, crashes } = await runUploadServer('http2', 25) expect(crashes).toEqual([]) expect(handled).toBe(25) diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index 2ca30f6..e0f9b50 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -14,6 +14,9 @@ import { IncomingMessage } from 'node:http' * after cancel. The per-chunk copy detaches chunks from Node's pooled `Buffer` * memory. * + * Fixed upstream in Node 26.10 (nodejs/node#62773); switch back to + * `Readable.toWeb` once every supported Node release has the fix. + * * Cancel destroys the source, except http1 server requests: they share their * socket with the response, so destroying them would kill an in-flight * response. They are abandoned instead — stalled by backpressure and reclaimed