From f617556bda673795ec887e3632593295ce4a6e0f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 13 May 2026 16:39:31 +0000 Subject: [PATCH] test: fix flaky http2-dispatcher WebSocket upgrade tests The CONNECT/WebSocket upgrade server handlers used stream.respond() with the default endStream: true, immediately closing the tunnel from the server side. This caused ECONNRESET because the HTTP/2 session would tear down the TCP connection before queued requests could complete or before test assertions could verify socket state. Changes: - Dispatcher#Upgrade: respond with { endStream: false } to keep the CONNECT tunnel open, move socket error handler before assertions - Dispatcher#Upgrade resumes queued requests: same endStream fix, add server-side end handler to close tunnel when client ends, move error handler before assertions, simplify try-finally cleanup Fixes: https://github.com/nodejs/undici/issues/5195 --- test/http2-dispatcher.js | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/http2-dispatcher.js b/test/http2-dispatcher.js index bbdd2e112a3..40390e6efa1 100644 --- a/test/http2-dispatcher.js +++ b/test/http2-dispatcher.js @@ -265,10 +265,11 @@ test('Dispatcher#Upgrade', async t => { t.fail(err) }) - stream.respond({ ':status': 200 }) + stream.respond({ ':status': 200 }, { endStream: false }) stream.resume() - - stream.end() + stream.once('end', () => { + stream.end() + }) }) await once(server.listen(0), 'listening') @@ -281,6 +282,7 @@ test('Dispatcher#Upgrade', async t => { }) const { socket } = await client.upgrade({ path: '/', protocol: 'websocket' }) + socket.on('error', () => {}) t.ok(socket.readable) t.ok(socket.writable) @@ -288,7 +290,6 @@ test('Dispatcher#Upgrade', async t => { await t.completed - socket.on('error', () => {}) socket.end() await once(socket, 'close') await client.close() @@ -311,7 +312,11 @@ test('Dispatcher#Upgrade resumes queued requests after successful WebSocket upgr }) if (headers[':method'] === 'CONNECT' && headers[':protocol'] === 'websocket') { - stream.respond({ ':status': 200 }) + stream.respond({ ':status': 200 }, { endStream: false }) + stream.resume() + stream.once('end', () => { + stream.end() + }) return } @@ -337,27 +342,22 @@ test('Dispatcher#Upgrade resumes queued requests after successful WebSocket upgr after(() => client.close()) after(() => server.close()) - let upgradeSocket + const upgrade = client.upgrade({ path: '/', protocol: 'websocket' }) + const post = client.request({ method: 'POST', path: '/', body: 'hello' }) - try { - const upgrade = client.upgrade({ path: '/', protocol: 'websocket' }) - const post = client.request({ method: 'POST', path: '/', body: 'hello' }) - - const { socket } = await upgrade - upgradeSocket = socket - t.strictEqual(socket.closed, false) - - const response = await Promise.race([ - post, - sleep(1_000).then(() => null) - ]) - - t.ok(postReachedServer) - t.strictEqual(response?.statusCode, 200) - } finally { - upgradeSocket?.on('error', () => {}) - upgradeSocket?.end() - } + const { socket } = await upgrade + socket.on('error', () => {}) + t.strictEqual(socket.closed, false) + + const response = await Promise.race([ + post, + sleep(1_000).then(() => null) + ]) + + t.ok(postReachedServer) + t.strictEqual(response?.statusCode, 200) + + socket.end() await t.completed })