From 5a11eb8b5386e023934d94d15b8f2d24c953a5b5 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 14:31:51 -0600 Subject: [PATCH 01/12] test: add unexpected disconnect guards to more client test files Signed-off-by: Sam Mayer Signed-off-by: Sam Mayer --- test/client-write-max-listeners.js | 6 ++++++ test/connect-pre-shared-session.js | 6 ++++++ test/https.js | 12 ++++++++++++ test/issue-803.js | 6 ++++++ test/max-headers.js | 6 ++++++ test/pipeline-pipelining.js | 6 ++++++ test/request-timeout2.js | 6 ++++++ test/socket-back-pressure.js | 6 ++++++ test/stream-compat.js | 12 ++++++++++++ test/trailers.js | 14 ++++++++++++++ 10 files changed, 80 insertions(+) diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index 09ab13cafb8..abfe204db74 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -48,6 +48,12 @@ test('socket close listener does not leak', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + for (let n = 0; n < 16; ++n) { client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) } diff --git a/test/connect-pre-shared-session.js b/test/connect-pre-shared-session.js index 30031048da7..5bc811ff7c5 100644 --- a/test/connect-pre-shared-session.js +++ b/test/connect-pre-shared-session.js @@ -31,6 +31,12 @@ test('custom session passed to client will be used in tls connect call', async ( }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' diff --git a/test/https.js b/test/https.js index dd5078bd2d8..04b8501be16 100644 --- a/test/https.js +++ b/test/https.js @@ -25,6 +25,12 @@ test('https get with tls opts', async (t) => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) @@ -61,6 +67,12 @@ test('https get with tls opts ip', async (t) => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) diff --git a/test/issue-803.js b/test/issue-803.js index 12540100f8d..33f59804792 100644 --- a/test/issue-803.js +++ b/test/issue-803.js @@ -39,6 +39,12 @@ test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' diff --git a/test/max-headers.js b/test/max-headers.js index 9edd8d1efdc..60e5609ea1d 100644 --- a/test/max-headers.js +++ b/test/max-headers.js @@ -25,6 +25,12 @@ test('handle a lot of headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index bb4cf22e84f..96452bfaeac 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -22,6 +22,12 @@ test('pipeline pipelining', async (t) => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client[kConnect](() => { t.equal(client[kRunning], 0) client.pipeline({ diff --git a/test/request-timeout2.js b/test/request-timeout2.js index 48293f81e95..e58f2f84264 100644 --- a/test/request-timeout2.js +++ b/test/request-timeout2.js @@ -25,6 +25,12 @@ test('request timeout with slow readable body', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const body = new Readable({ read () { if (this._reading) { diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index e7150c0a9c5..25899123ab5 100644 --- a/test/socket-back-pressure.js +++ b/test/socket-back-pressure.js @@ -37,6 +37,12 @@ test('socket back-pressure', async (t) => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => { t.ifError(err) data.body diff --git a/test/stream-compat.js b/test/stream-compat.js index 15806a2447a..9b83b70db59 100644 --- a/test/stream-compat.js +++ b/test/stream-compat.js @@ -50,6 +50,12 @@ test('IncomingMessage', async (t) => { const proxyClient = new Client(`http://localhost:${server.address().port}`) after(() => proxyClient.destroy()) + proxyClient.on('disconnect', () => { + if (!proxyClient.closed && !proxyClient.destroyed) { + t.fail('unexpected disconnect') + } + }) + const proxy = createServer({ joinDuplicateHeaders: true }, (req, res) => { proxyClient.request({ path: '/', @@ -66,6 +72,12 @@ test('IncomingMessage', async (t) => { const client = new Client(`http://localhost:${proxy.address().port}`) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', diff --git a/test/trailers.js b/test/trailers.js index 64a29da7b22..6815632271d 100644 --- a/test/trailers.js +++ b/test/trailers.js @@ -18,6 +18,13 @@ test('response trailers missing is OK', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const { body } = await client.request({ path: '/', method: 'GET', @@ -46,6 +53,13 @@ test('response trailers missing w trailers is OK', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const { body, trailers } = await client.request({ path: '/', method: 'GET', From 69f09b77721f742fce84a1e85fc219fe7cade7ce Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 14:47:49 -0600 Subject: [PATCH 02/12] test: add unexpected disconnect guards to remaining client test files Signed-off-by: Sam Mayer --- test/client-timeout.js | 6 ++++++ test/http-100.js | 12 ++++++++++++ test/parser-issues.js | 6 ++++++ test/promises.js | 42 ++++++++++++++++++++++++++++++++++++++++++ test/proxy.js | 24 ++++++++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/test/client-timeout.js b/test/client-timeout.js index 26cf21913c2..a2c18cd8fd0 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -180,6 +180,12 @@ test('parser resume with no body timeout', async (t) => { }) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.dispatch({ path: '/', method: 'GET' diff --git a/test/http-100.js b/test/http-100.js index 055a01faaa5..96dd48d4de9 100644 --- a/test/http-100.js +++ b/test/http-100.js @@ -21,6 +21,12 @@ test('ignore informational response', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'POST', @@ -137,6 +143,12 @@ test('1xx response without timeouts', async t => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'POST', diff --git a/test/parser-issues.js b/test/parser-issues.js index 2d9f04628de..859a5f096cb 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -26,6 +26,12 @@ test('https://github.com/mcollina/undici/issues/268', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ method: 'GET', path: '/nxt/_changes?feed=continuous&heartbeat=5000', diff --git a/test/promises.js b/test/promises.js index 3aaf909cd71..456a3a6b254 100644 --- a/test/promises.js +++ b/test/promises.js @@ -22,6 +22,12 @@ test('basic get, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) t.strictEqual(statusCode, 200) @@ -70,6 +76,12 @@ test('basic POST with string, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) t.strictEqual(statusCode, 200) @@ -100,6 +112,12 @@ test('basic POST with Buffer, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) t.strictEqual(statusCode, 200) @@ -130,6 +148,12 @@ test('basic POST with stream, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, body } = await client.request({ path: '/', @@ -167,6 +191,12 @@ test('basic POST with async-iterator, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, body } = await client.request({ path: '/', @@ -227,6 +257,12 @@ test('20 times GET with pipelining 10, async await support', async (t) => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + for (let i = 0; i < num; i++) { makeRequest(i) } @@ -275,6 +311,12 @@ test('pool, async await support', async (t) => { const client = new Pool(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) t.strictEqual(statusCode, 200) diff --git a/test/proxy.js b/test/proxy.js index 86cae7bfc93..9d209b6be10 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -23,6 +23,12 @@ test('connect through proxy', async (t) => { const client = new Client(proxyUrl) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar' @@ -62,6 +68,12 @@ test('connect through proxy with auth', async (t) => { const client = new Client(proxyUrl) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar', @@ -102,6 +114,12 @@ test('connect through proxy with auth but invalid credentials', async (t) => { const client = new Client(proxyUrl) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar', @@ -134,6 +152,12 @@ test('connect through proxy (with pool)', async (t) => { const pool = new Pool(proxyUrl) + pool.on('disconnect', () => { + if (!pool.closed && !pool.destroyed) { + t.fail('unexpected disconnect') + } + }) + const response = await pool.request({ method: 'GET', path: serverUrl + '/hello?foo=bar' From c4ee3db49402a713f05be9a1fa7ab666523aa77c Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 14:53:16 -0600 Subject: [PATCH 03/12] Document use of guard-clause in tests --- docs/docs/best-practices/writing-tests.md | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 57549de6357..40939542e41 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -18,3 +18,46 @@ const agent = new Agent({ setGlobalDispatcher(agent) ``` + +## Guarding against unexpected disconnects + +Undici's `Client` automatically reconnects after a socket error. This means +a test can silently disconnect, reconnect, and still pass. Unfortunately, +this could mask bugs like unexpected parser errors or protocol violations. +To catch these silent reconnections, add a disconnect guard after creating +a `Client`: + +```js +const { Client } = require('undici') +const { test, after } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') + +test('example with disconnect guard', async (t) => { + t = tspl(t, { plan: 1 }) + + const client = new Client('http://localhost:3000') + after(() => client.close()) + + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + + // ... test logic ... +}) +``` + +`client.close()` and `client.destroy()` both emit `'disconnect'` events, but +those are expected. The guard only fails when a disconnect happens during the +active test (i.e., `!client.closed && !client.destroyed` is true). + +Skip the guard for tests where a disconnect is expected behavior, such as: + +- Signal aborts (`signal.emit('abort')`, `ac.abort()`) +- Server-side destruction (`res.destroy()`, `req.socket.destroy()`) +- Client-side body destruction mid-stream (`data.body.destroy()`) +- Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`) +- Successful upgrades (the socket is detached from the `Client`) +- Retry/reconnect tests where the disconnect triggers the retry +- HTTP parser errors from malformed responses (`HTTPParserError`) From ce9f027b756e620daa9bf7490f6ec5e748a36cbb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 15:05:31 -0600 Subject: [PATCH 04/12] test: add unexpected disconnect guards to content-length and header test files Signed-off-by: Sam Mayer --- test/content-length.js | 12 +++++++++++ test/headers-as-array.js | 36 ++++++++++++++++++++++++++++++++ test/headers-crlf.js | 6 ++++++ test/max-response-size.js | 12 +++++++++++ test/no-strict-content-length.js | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+) diff --git a/test/content-length.js b/test/content-length.js index 6d8e7799c91..92386b93b67 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -209,6 +209,12 @@ test('request streaming no body data when content-length=0', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -278,6 +284,12 @@ test('request streaming with Readable.from(buf)', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', diff --git a/test/headers-as-array.js b/test/headers-as-array.js index 9fd4e738af8..a49ccbf3a70 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -20,6 +20,12 @@ test('handle headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', @@ -46,6 +52,12 @@ test('handle multi-valued headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', @@ -72,6 +84,12 @@ test('handle headers with array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', @@ -98,6 +116,12 @@ test('handle multi-valued headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', @@ -118,6 +142,12 @@ test('fail if headers array is odd', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', @@ -141,6 +171,12 @@ test('fail if headers is not an object or an array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET', diff --git a/test/headers-crlf.js b/test/headers-crlf.js index ee440e06045..2eedab56db6 100644 --- a/test/headers-crlf.js +++ b/test/headers-crlf.js @@ -21,6 +21,12 @@ test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' try { diff --git a/test/max-response-size.js b/test/max-response-size.js index c1b39c98204..643a76cc031 100644 --- a/test/max-response-size.js +++ b/test/max-response-size.js @@ -23,6 +23,12 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) @@ -56,6 +62,12 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index 3b16d2eeb72..ecc9acaaf0c 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -163,6 +163,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -203,6 +209,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -243,6 +255,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -283,6 +301,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -323,6 +347,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -362,6 +392,12 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', From 167e6318701fe8f6ffe87b20705995b38bd7edc0 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 15:10:45 -0600 Subject: [PATCH 05/12] create guardDisconnect() helper --- docs/docs/best-practices/writing-tests.md | 18 +++--- test/client-connect.js | 7 +-- test/client-pipelining.js | 49 +++------------ test/client-post.js | 13 +--- test/client-stream.js | 73 ++++------------------- test/client-timeout.js | 7 +-- test/client-write-max-listeners.js | 7 +-- test/connect-pre-shared-session.js | 7 +-- test/content-length.js | 13 +--- test/guard-disconnect.js | 40 +++++++++++++ test/headers-as-array.js | 37 +++--------- test/headers-crlf.js | 7 +-- test/http-100.js | 13 +--- test/https.js | 13 +--- test/issue-803.js | 7 +-- test/max-headers.js | 7 +-- test/max-response-size.js | 13 +--- test/no-strict-content-length.js | 37 +++--------- test/parser-issues.js | 7 +-- test/pipeline-pipelining.js | 7 +-- test/promises.js | 43 +++---------- test/proxy.js | 25 ++------ test/request-timeout2.js | 7 +-- test/socket-back-pressure.js | 7 +-- test/stream-compat.js | 13 +--- test/trailers.js | 13 +--- 26 files changed, 140 insertions(+), 350 deletions(-) create mode 100644 test/guard-disconnect.js diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 40939542e41..43d90b71758 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -24,13 +24,14 @@ setGlobalDispatcher(agent) Undici's `Client` automatically reconnects after a socket error. This means a test can silently disconnect, reconnect, and still pass. Unfortunately, this could mask bugs like unexpected parser errors or protocol violations. -To catch these silent reconnections, add a disconnect guard after creating -a `Client`: +To catch these silent reconnections, use the `guardDisconnect` helper from +`test/guard-disconnect.js` after creating a `Client` (or `Pool`): ```js const { Client } = require('undici') const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') +const { guardDisconnect } = require('./guard-disconnect') test('example with disconnect guard', async (t) => { t = tspl(t, { plan: 1 }) @@ -38,19 +39,16 @@ test('example with disconnect guard', async (t) => { const client = new Client('http://localhost:3000') after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) // ... test logic ... }) ``` -`client.close()` and `client.destroy()` both emit `'disconnect'` events, but -those are expected. The guard only fails when a disconnect happens during the -active test (i.e., `!client.closed && !client.destroyed` is true). +The guard listens for `'disconnect'` events and calls `t.fail()` unless the +client is already closing or destroyed. `client.close()` and `client.destroy()` +both emit `'disconnect'` events, but those are expected — the guard skips them +by checking `!client.closed && !client.destroyed`. Skip the guard for tests where a disconnect is expected behavior, such as: diff --git a/test/client-connect.js b/test/client-connect.js index da155bc3d05..412c6e47444 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -7,6 +7,7 @@ const { Client, errors } = require('..') const http = require('node:http') const EE = require('node:events') const { kBusy } = require('../lib/core/symbols') +const { guardDisconnect } = require('./guard-disconnect') // TODO: move to test/node-test/client-connect.js test('connect aborted after connect', async (t) => { @@ -29,11 +30,7 @@ test('connect aborted after connect', async (t) => { pipelining: 3 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.connect({ path: '/', diff --git a/test/client-pipelining.js b/test/client-pipelining.js index e21a7c2f6e0..9b761608af3 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -9,6 +9,7 @@ const { kConnect } = require('../lib/core/symbols') const EE = require('node:events') const { kBusy, kRunning, kSize } = require('../lib/core/symbols') const { maybeWrapStream, consts } = require('./utils/async-iterators') +const { guardDisconnect } = require('./guard-disconnect') test('20 times GET with pipelining 10', async (t) => { const num = 20 @@ -41,11 +42,7 @@ test('20 times GET with pipelining 10', async (t) => { pipelining: 10 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) for (let i = 0; i < num; i++) { makeRequest(i) @@ -103,11 +100,7 @@ test('A client should enqueue as much as twice its pipelining factor', async (t) pipelining: 2 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) for (; sent < 2;) { t.ok(client[kSize] <= client.pipelining, 'client is not full') @@ -158,11 +151,7 @@ test('pipeline 1 is 1 active request', async (t) => { pipelining: 1 }) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET' @@ -216,11 +205,7 @@ test('pipelined chunked POST stream', async (t) => { pipelining: 2 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -290,11 +275,7 @@ test('pipelined chunked POST iterator', async (t) => { pipelining: 2 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -418,11 +399,7 @@ test('pipelining non-idempotent', async (t) => { pipelining: 2 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) let ended = false client.request({ @@ -469,11 +446,7 @@ function pipeliningNonIdempotentWithBody (bodyType) { pipelining: 2 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) let ended = false let reading = false @@ -782,11 +755,7 @@ test('pipelining blocked', async (t) => { pipelining: 10 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET', diff --git a/test/client-post.js b/test/client-post.js index 10350e031ee..11fd76a6ce2 100644 --- a/test/client-post.js +++ b/test/client-post.js @@ -5,6 +5,7 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') test('request post blob', async (t) => { t = tspl(t, { plan: 3 }) @@ -26,11 +27,7 @@ test('request post blob', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -67,11 +64,7 @@ test('request post arrayBuffer', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const buf = Buffer.from('asd') const dst = new ArrayBuffer(buf.byteLength) diff --git a/test/client-stream.js b/test/client-stream.js index 24d64bf822c..0a9667ea95c 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -6,6 +6,7 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const { PassThrough, Writable, Readable } = require('node:stream') const EE = require('node:events') +const { guardDisconnect } = require('./guard-disconnect') test('stream get', async (t) => { t = tspl(t, { plan: 9 }) @@ -22,11 +23,7 @@ test('stream get', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const signal = new EE() client.stream({ @@ -70,11 +67,7 @@ test('stream promise get', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) await client.stream({ path: '/', @@ -264,11 +257,7 @@ test('stream waits only for writable side', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const pt = new PassThrough({ autoDestroy: false }) client.stream({ @@ -336,11 +325,7 @@ test('stream destroy if not readable', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.stream({ path: '/', @@ -417,11 +402,7 @@ test('stream body without destroy', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.stream({ path: '/', @@ -545,11 +526,7 @@ test('stream abort after complete', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const pt = new PassThrough() const signal = new EE() @@ -610,11 +587,7 @@ test('trailers', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.stream({ path: '/', @@ -640,11 +613,7 @@ test('stream ignore 1xx', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) let buf = '' client.stream({ @@ -677,11 +646,7 @@ test('stream ignore 1xx and use onInfo', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) let buf = '' client.stream({ @@ -720,11 +685,7 @@ test('stream backpressure', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) let buf = '' client.stream({ @@ -786,11 +747,7 @@ test('stream needDrain', async (t) => { after(() => { client.destroy() }) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const dst = new PassThrough() dst.pause() @@ -847,11 +804,7 @@ test('stream legacy needDrain', async (t) => { after(() => { client.destroy() }) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const dst = new PassThrough() dst.pause() diff --git a/test/client-timeout.js b/test/client-timeout.js index a2c18cd8fd0..23d0a1ee831 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -7,6 +7,7 @@ const { createServer } = require('node:http') const { Readable } = require('node:stream') const FakeTimers = require('@sinonjs/fake-timers') const timers = require('../lib/util/timers') +const { guardDisconnect } = require('./guard-disconnect') test('refresh timeout on pause', async (t) => { t = tspl(t, { plan: 1 }) @@ -180,11 +181,7 @@ test('parser resume with no body timeout', async (t) => { }) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.dispatch({ path: '/', diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index abfe204db74..02793fe6a93 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -6,6 +6,7 @@ const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') +const { guardDisconnect } = require('./guard-disconnect') test('socket close listener does not leak', async (t) => { t = tspl(t, { plan: 32 }) @@ -48,11 +49,7 @@ test('socket close listener does not leak', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) for (let n = 0; n < 16; ++n) { client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) diff --git a/test/connect-pre-shared-session.js b/test/connect-pre-shared-session.js index 5bc811ff7c5..e1c5f8fc76f 100644 --- a/test/connect-pre-shared-session.js +++ b/test/connect-pre-shared-session.js @@ -6,6 +6,7 @@ const { Client } = require('..') const { createServer } = require('node:https') const pem = require('@metcoder95/https-pem') const tls = require('node:tls') +const { guardDisconnect } = require('./guard-disconnect') test('custom session passed to client will be used in tls connect call', async (t) => { t = tspl(t, { plan: 6 }) @@ -31,11 +32,7 @@ test('custom session passed to client will be used in tls connect call', async ( }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const { statusCode, headers, body } = await client.request({ path: '/', diff --git a/test/content-length.js b/test/content-length.js index 92386b93b67..a03cbd793d8 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -6,6 +6,7 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { maybeWrapStream, consts } = require('./utils/async-iterators') +const { guardDisconnect } = require('./guard-disconnect') test('request invalid content-length', async (t) => { t = tspl(t, { plan: 7 }) @@ -209,11 +210,7 @@ test('request streaming no body data when content-length=0', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -284,11 +281,7 @@ test('request streaming with Readable.from(buf)', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/guard-disconnect.js b/test/guard-disconnect.js new file mode 100644 index 00000000000..94bbff8e8a4 --- /dev/null +++ b/test/guard-disconnect.js @@ -0,0 +1,40 @@ +'use strict' + +const { describe, test } = require('node:test') +const { EventEmitter } = require('node:events') +const { strictEqual } = require('node:assert') + +function guardDisconnect (dispatcher, t) { + dispatcher.on('disconnect', () => { + if (!dispatcher.closed && !dispatcher.destroyed) { + t.fail('unexpected disconnect') + } + }) +} + +describe('guardDisconnect', () => { + const cases = [ + { closed: false, destroyed: false, shouldFail: true, label: 'active dispatcher' }, + { closed: true, destroyed: false, shouldFail: false, label: 'closed dispatcher' }, + { closed: false, destroyed: true, shouldFail: false, label: 'destroyed dispatcher' }, + { closed: true, destroyed: true, shouldFail: false, label: 'closed and destroyed dispatcher' } + ] + + for (const { closed, destroyed, shouldFail, label } of cases) { + test(`${shouldFail ? 'calls' : 'does not call'} t.fail for ${label}`, () => { + const dispatcher = new EventEmitter() + dispatcher.closed = closed + dispatcher.destroyed = destroyed + + let failReason = null + const t = { fail: (reason) => { failReason = reason } } + + guardDisconnect(dispatcher, t) + dispatcher.emit('disconnect') + + strictEqual(failReason, shouldFail ? 'unexpected disconnect' : null) + }) + } +}) + +module.exports = { guardDisconnect } diff --git a/test/headers-as-array.js b/test/headers-as-array.js index a49ccbf3a70..edeca411f27 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -4,6 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') test('handle headers as array', async (t) => { t = tspl(t, { plan: 3 }) @@ -20,11 +21,7 @@ test('handle headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -52,11 +49,7 @@ test('handle multi-valued headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -84,11 +77,7 @@ test('handle headers with array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -116,11 +105,7 @@ test('handle multi-valued headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -142,11 +127,7 @@ test('fail if headers array is odd', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -171,11 +152,7 @@ test('fail if headers is not an object or an array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/headers-crlf.js b/test/headers-crlf.js index 2eedab56db6..91d3b557fa9 100644 --- a/test/headers-crlf.js +++ b/test/headers-crlf.js @@ -5,6 +5,7 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { t = tspl(t, { plan: 1 }) @@ -21,11 +22,7 @@ test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' diff --git a/test/http-100.js b/test/http-100.js index 96dd48d4de9..0b2979e3c9a 100644 --- a/test/http-100.js +++ b/test/http-100.js @@ -6,6 +6,7 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const net = require('node:net') const { once } = require('node:events') +const { guardDisconnect } = require('./guard-disconnect') test('ignore informational response', async (t) => { t = tspl(t, { plan: 2 }) @@ -21,11 +22,7 @@ test('ignore informational response', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -143,11 +140,7 @@ test('1xx response without timeouts', async t => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/https.js b/test/https.js index 04b8501be16..16699a2e60d 100644 --- a/test/https.js +++ b/test/https.js @@ -5,6 +5,7 @@ const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') const pem = require('@metcoder95/https-pem') +const { guardDisconnect } = require('./guard-disconnect') test('https get with tls opts', async (t) => { t = tspl(t, { plan: 6 }) @@ -25,11 +26,7 @@ test('https get with tls opts', async (t) => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) @@ -67,11 +64,7 @@ test('https get with tls opts ip', async (t) => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) diff --git a/test/issue-803.js b/test/issue-803.js index 33f59804792..f7e4a047c06 100644 --- a/test/issue-803.js +++ b/test/issue-803.js @@ -5,6 +5,7 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t) => { t = tspl(t, { plan: 2 }) @@ -39,11 +40,7 @@ test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/max-headers.js b/test/max-headers.js index 60e5609ea1d..386614895e4 100644 --- a/test/max-headers.js +++ b/test/max-headers.js @@ -5,6 +5,7 @@ const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { once } = require('node:events') +const { guardDisconnect } = require('./guard-disconnect') test('handle a lot of headers', async (t) => { t = tspl(t, { plan: 3 }) @@ -25,11 +26,7 @@ test('handle a lot of headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/max-response-size.js b/test/max-response-size.js index 643a76cc031..aec327c427d 100644 --- a/test/max-response-size.js +++ b/test/max-response-size.js @@ -4,6 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after, describe } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') describe('max response size', async (t) => { test('default max default size should allow all responses', async (t) => { @@ -23,11 +24,7 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) @@ -62,11 +59,7 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index ecc9acaaf0c..8720bd9fc7a 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -7,6 +7,7 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { wrapWithAsyncIterable } = require('./utils/async-iterators') +const { guardDisconnect } = require('./guard-disconnect') describe('strictContentLength: false', () => { const emitWarningOriginal = process.emitWarning @@ -163,11 +164,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -209,11 +206,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -255,11 +248,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -301,11 +290,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -347,11 +332,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', @@ -392,11 +373,7 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/parser-issues.js b/test/parser-issues.js index 859a5f096cb..97031c19f55 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -4,6 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const net = require('node:net') const { Client, errors } = require('..') +const { guardDisconnect } = require('./guard-disconnect') test('https://github.com/mcollina/undici/issues/268', async (t) => { t = tspl(t, { plan: 2 }) @@ -26,11 +27,7 @@ test('https://github.com/mcollina/undici/issues/268', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ method: 'GET', diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index 96452bfaeac..7af892fe32c 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -6,6 +6,7 @@ const { Client } = require('..') const { createServer } = require('node:http') const { kConnect } = require('../lib/core/symbols') const { kBusy, kPending, kRunning } = require('../lib/core/symbols') +const { guardDisconnect } = require('./guard-disconnect') test('pipeline pipelining', async (t) => { t = tspl(t, { plan: 10 }) @@ -22,11 +23,7 @@ test('pipeline pipelining', async (t) => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client[kConnect](() => { t.equal(client[kRunning], 0) diff --git a/test/promises.js b/test/promises.js index 456a3a6b254..1d6ba55d596 100644 --- a/test/promises.js +++ b/test/promises.js @@ -6,6 +6,7 @@ const { Client, Pool } = require('..') const { createServer } = require('node:http') const { readFileSync, createReadStream } = require('node:fs') const { wrapWithAsyncIterable } = require('./utils/async-iterators') +const { guardDisconnect } = require('./guard-disconnect') test('basic get, async await support', async (t) => { t = tspl(t, { plan: 5 }) @@ -22,11 +23,7 @@ test('basic get, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) @@ -76,11 +73,7 @@ test('basic POST with string, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) @@ -112,11 +105,7 @@ test('basic POST with Buffer, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) @@ -148,11 +137,7 @@ test('basic POST with stream, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, body } = await client.request({ @@ -191,11 +176,7 @@ test('basic POST with async-iterator, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, body } = await client.request({ @@ -257,11 +238,7 @@ test('20 times GET with pipelining 10, async await support', async (t) => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) for (let i = 0; i < num; i++) { makeRequest(i) @@ -311,11 +288,7 @@ test('pool, async await support', async (t) => { const client = new Pool(`http://localhost:${server.address().port}`) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) diff --git a/test/proxy.js b/test/proxy.js index 9d209b6be10..d530c3a789f 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -5,6 +5,7 @@ const { test } = require('node:test') const { Client, Pool } = require('..') const { createServer } = require('node:http') const { createProxy } = require('proxy') +const { guardDisconnect } = require('./guard-disconnect') test('connect through proxy', async (t) => { t = tspl(t, { plan: 3 }) @@ -23,11 +24,7 @@ test('connect through proxy', async (t) => { const client = new Client(proxyUrl) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const response = await client.request({ method: 'GET', @@ -68,11 +65,7 @@ test('connect through proxy with auth', async (t) => { const client = new Client(proxyUrl) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const response = await client.request({ method: 'GET', @@ -114,11 +107,7 @@ test('connect through proxy with auth but invalid credentials', async (t) => { const client = new Client(proxyUrl) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const response = await client.request({ method: 'GET', @@ -152,11 +141,7 @@ test('connect through proxy (with pool)', async (t) => { const pool = new Pool(proxyUrl) - pool.on('disconnect', () => { - if (!pool.closed && !pool.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(pool, t) const response = await pool.request({ method: 'GET', diff --git a/test/request-timeout2.js b/test/request-timeout2.js index e58f2f84264..5772a80d963 100644 --- a/test/request-timeout2.js +++ b/test/request-timeout2.js @@ -6,6 +6,7 @@ const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') +const { guardDisconnect } = require('./guard-disconnect') test('request timeout with slow readable body', async (t) => { t = tspl(t, { plan: 1 }) @@ -25,11 +26,7 @@ test('request timeout with slow readable body', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const body = new Readable({ read () { diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index 25899123ab5..0a2fabe9261 100644 --- a/test/socket-back-pressure.js +++ b/test/socket-back-pressure.js @@ -6,6 +6,7 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { test, after } = require('node:test') +const { guardDisconnect } = require('./guard-disconnect') test('socket back-pressure', async (t) => { t = tspl(t, { plan: 3 }) @@ -37,11 +38,7 @@ test('socket back-pressure', async (t) => { }) after(() => client.close()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => { t.ifError(err) diff --git a/test/stream-compat.js b/test/stream-compat.js index 9b83b70db59..c3aac381197 100644 --- a/test/stream-compat.js +++ b/test/stream-compat.js @@ -6,6 +6,7 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const EE = require('node:events') +const { guardDisconnect } = require('./guard-disconnect') test('stream body without destroy', async (t) => { t = tspl(t, { plan: 2 }) @@ -50,11 +51,7 @@ test('IncomingMessage', async (t) => { const proxyClient = new Client(`http://localhost:${server.address().port}`) after(() => proxyClient.destroy()) - proxyClient.on('disconnect', () => { - if (!proxyClient.closed && !proxyClient.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(proxyClient, t) const proxy = createServer({ joinDuplicateHeaders: true }, (req, res) => { proxyClient.request({ @@ -72,11 +69,7 @@ test('IncomingMessage', async (t) => { const client = new Client(`http://localhost:${proxy.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) client.request({ path: '/', diff --git a/test/trailers.js b/test/trailers.js index 6815632271d..150b47c8604 100644 --- a/test/trailers.js +++ b/test/trailers.js @@ -4,6 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') +const { guardDisconnect } = require('./guard-disconnect') test('response trailers missing is OK', async (t) => { t = tspl(t, { plan: 1 }) @@ -19,11 +20,7 @@ test('response trailers missing is OK', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const { body } = await client.request({ path: '/', @@ -54,11 +51,7 @@ test('response trailers missing w trailers is OK', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - client.on('disconnect', () => { - if (!client.closed && !client.destroyed) { - t.fail('unexpected disconnect') - } - }) + guardDisconnect(client, t) const { body, trailers } = await client.request({ path: '/', From dc05ff93c8e45af033f473632d6ea2e77c965b84 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 15:30:28 -0600 Subject: [PATCH 06/12] Apply suggestion from @samayer12 --- docs/docs/best-practices/writing-tests.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 43d90b71758..f1c696564a0 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -46,11 +46,7 @@ test('example with disconnect guard', async (t) => { ``` The guard listens for `'disconnect'` events and calls `t.fail()` unless the -client is already closing or destroyed. `client.close()` and `client.destroy()` -both emit `'disconnect'` events, but those are expected — the guard skips them -by checking `!client.closed && !client.destroyed`. - -Skip the guard for tests where a disconnect is expected behavior, such as: +client is already closing or destroyed. Skip the guard for tests where a disconnect is expected behavior, such as: - Signal aborts (`signal.emit('abort')`, `ac.abort()`) - Server-side destruction (`res.destroy()`, `req.socket.destroy()`) From 7e0fbba2e152f6e04a15f78f84bbf06ac6e57940 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 27 Feb 2026 16:09:25 -0600 Subject: [PATCH 07/12] fix(test): use explicit dispatcher in content-encoding tests The first describe block used the global agent, leaving pooled connections alive after server.closeAllConnections() sent RST. On macOS, the async RST delivery could surface as ECONNRESET in the next describe block's fetch call. Give each block its own Client and await cleanup to eliminate the race. Signed-off-by: Sam Mayer --- test/fetch/encoding.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 893a24886e4..41fe4593fe2 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -10,6 +10,7 @@ describe('content-encoding handling', () => { const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64') let server + let client before(async () => { server = createServer({ noDelay: true @@ -47,16 +48,19 @@ describe('content-encoding handling', () => { } }) await once(server.listen(0), 'listening') + client = new Client(`http://localhost:${server.address().port}`) }) - after(() => { + after(async () => { + await client.close() server.closeAllConnections?.() server.close() + await once(server, 'close') }) test('content-encoding header', async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - keepalive: false, + dispatcher: client, headers: { 'accept-encoding': 'deflate, gzip' } }) @@ -67,7 +71,7 @@ describe('content-encoding handling', () => { test('content-encoding header is case-iNsENsITIve', async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - keepalive: false, + dispatcher: client, headers: { 'accept-encoding': 'DeFlAtE, GzIp' } }) @@ -80,7 +84,7 @@ describe('content-encoding handling', () => { { skip: typeof require('node:zlib').createZstdDecompress !== 'function' }, async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - keepalive: false, + dispatcher: client, headers: { 'accept-encoding': 'zstd' } }) From 480b136a8f9130106d72ba5e1af76e069f22a2b1 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 2 Mar 2026 09:48:52 -0600 Subject: [PATCH 08/12] Revert "Apply suggestion from @samayer12" This reverts commit 29ddaeb831a3b221f7f5072610d65a681f2b46d6. --- docs/docs/best-practices/writing-tests.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index f1c696564a0..43d90b71758 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -46,7 +46,11 @@ test('example with disconnect guard', async (t) => { ``` The guard listens for `'disconnect'` events and calls `t.fail()` unless the -client is already closing or destroyed. Skip the guard for tests where a disconnect is expected behavior, such as: +client is already closing or destroyed. `client.close()` and `client.destroy()` +both emit `'disconnect'` events, but those are expected — the guard skips them +by checking `!client.closed && !client.destroyed`. + +Skip the guard for tests where a disconnect is expected behavior, such as: - Signal aborts (`signal.emit('abort')`, `ac.abort()`) - Server-side destruction (`res.destroy()`, `req.socket.destroy()`) From 0bd6b3025304afd37b2a49b5455d18aa3f343a6e Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 2 Mar 2026 09:48:52 -0600 Subject: [PATCH 09/12] Revert "create guardDisconnect() helper" This reverts commit 78d583d317944158cd78de3998acd8475e5f422a. --- docs/docs/best-practices/writing-tests.md | 18 +++--- test/client-connect.js | 7 ++- test/client-pipelining.js | 49 ++++++++++++--- test/client-post.js | 13 +++- test/client-stream.js | 73 +++++++++++++++++++---- test/client-timeout.js | 7 ++- test/client-write-max-listeners.js | 7 ++- test/connect-pre-shared-session.js | 7 ++- test/content-length.js | 13 +++- test/guard-disconnect.js | 40 ------------- test/headers-as-array.js | 37 +++++++++--- test/headers-crlf.js | 7 ++- test/http-100.js | 13 +++- test/https.js | 13 +++- test/issue-803.js | 7 ++- test/max-headers.js | 7 ++- test/max-response-size.js | 13 +++- test/no-strict-content-length.js | 37 +++++++++--- test/parser-issues.js | 7 ++- test/pipeline-pipelining.js | 7 ++- test/promises.js | 43 ++++++++++--- test/proxy.js | 25 ++++++-- test/request-timeout2.js | 7 ++- test/socket-back-pressure.js | 7 ++- test/stream-compat.js | 13 +++- test/trailers.js | 13 +++- 26 files changed, 350 insertions(+), 140 deletions(-) delete mode 100644 test/guard-disconnect.js diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 43d90b71758..40939542e41 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -24,14 +24,13 @@ setGlobalDispatcher(agent) Undici's `Client` automatically reconnects after a socket error. This means a test can silently disconnect, reconnect, and still pass. Unfortunately, this could mask bugs like unexpected parser errors or protocol violations. -To catch these silent reconnections, use the `guardDisconnect` helper from -`test/guard-disconnect.js` after creating a `Client` (or `Pool`): +To catch these silent reconnections, add a disconnect guard after creating +a `Client`: ```js const { Client } = require('undici') const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') -const { guardDisconnect } = require('./guard-disconnect') test('example with disconnect guard', async (t) => { t = tspl(t, { plan: 1 }) @@ -39,16 +38,19 @@ test('example with disconnect guard', async (t) => { const client = new Client('http://localhost:3000') after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) // ... test logic ... }) ``` -The guard listens for `'disconnect'` events and calls `t.fail()` unless the -client is already closing or destroyed. `client.close()` and `client.destroy()` -both emit `'disconnect'` events, but those are expected — the guard skips them -by checking `!client.closed && !client.destroyed`. +`client.close()` and `client.destroy()` both emit `'disconnect'` events, but +those are expected. The guard only fails when a disconnect happens during the +active test (i.e., `!client.closed && !client.destroyed` is true). Skip the guard for tests where a disconnect is expected behavior, such as: diff --git a/test/client-connect.js b/test/client-connect.js index 412c6e47444..da155bc3d05 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -7,7 +7,6 @@ const { Client, errors } = require('..') const http = require('node:http') const EE = require('node:events') const { kBusy } = require('../lib/core/symbols') -const { guardDisconnect } = require('./guard-disconnect') // TODO: move to test/node-test/client-connect.js test('connect aborted after connect', async (t) => { @@ -30,7 +29,11 @@ test('connect aborted after connect', async (t) => { pipelining: 3 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.connect({ path: '/', diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 9b761608af3..e21a7c2f6e0 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -9,7 +9,6 @@ const { kConnect } = require('../lib/core/symbols') const EE = require('node:events') const { kBusy, kRunning, kSize } = require('../lib/core/symbols') const { maybeWrapStream, consts } = require('./utils/async-iterators') -const { guardDisconnect } = require('./guard-disconnect') test('20 times GET with pipelining 10', async (t) => { const num = 20 @@ -42,7 +41,11 @@ test('20 times GET with pipelining 10', async (t) => { pipelining: 10 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (let i = 0; i < num; i++) { makeRequest(i) @@ -100,7 +103,11 @@ test('A client should enqueue as much as twice its pipelining factor', async (t) pipelining: 2 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (; sent < 2;) { t.ok(client[kSize] <= client.pipelining, 'client is not full') @@ -151,7 +158,11 @@ test('pipeline 1 is 1 active request', async (t) => { pipelining: 1 }) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' @@ -205,7 +216,11 @@ test('pipelined chunked POST stream', async (t) => { pipelining: 2 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -275,7 +290,11 @@ test('pipelined chunked POST iterator', async (t) => { pipelining: 2 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -399,7 +418,11 @@ test('pipelining non-idempotent', async (t) => { pipelining: 2 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let ended = false client.request({ @@ -446,7 +469,11 @@ function pipeliningNonIdempotentWithBody (bodyType) { pipelining: 2 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let ended = false let reading = false @@ -755,7 +782,11 @@ test('pipelining blocked', async (t) => { pipelining: 10 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET', diff --git a/test/client-post.js b/test/client-post.js index 11fd76a6ce2..10350e031ee 100644 --- a/test/client-post.js +++ b/test/client-post.js @@ -5,7 +5,6 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') test('request post blob', async (t) => { t = tspl(t, { plan: 3 }) @@ -27,7 +26,11 @@ test('request post blob', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -64,7 +67,11 @@ test('request post arrayBuffer', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const buf = Buffer.from('asd') const dst = new ArrayBuffer(buf.byteLength) diff --git a/test/client-stream.js b/test/client-stream.js index 0a9667ea95c..24d64bf822c 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -6,7 +6,6 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const { PassThrough, Writable, Readable } = require('node:stream') const EE = require('node:events') -const { guardDisconnect } = require('./guard-disconnect') test('stream get', async (t) => { t = tspl(t, { plan: 9 }) @@ -23,7 +22,11 @@ test('stream get', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const signal = new EE() client.stream({ @@ -67,7 +70,11 @@ test('stream promise get', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) await client.stream({ path: '/', @@ -257,7 +264,11 @@ test('stream waits only for writable side', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const pt = new PassThrough({ autoDestroy: false }) client.stream({ @@ -325,7 +336,11 @@ test('stream destroy if not readable', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -402,7 +417,11 @@ test('stream body without destroy', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -526,7 +545,11 @@ test('stream abort after complete', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const pt = new PassThrough() const signal = new EE() @@ -587,7 +610,11 @@ test('trailers', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -613,7 +640,11 @@ test('stream ignore 1xx', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -646,7 +677,11 @@ test('stream ignore 1xx and use onInfo', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -685,7 +720,11 @@ test('stream backpressure', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -747,7 +786,11 @@ test('stream needDrain', async (t) => { after(() => { client.destroy() }) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const dst = new PassThrough() dst.pause() @@ -804,7 +847,11 @@ test('stream legacy needDrain', async (t) => { after(() => { client.destroy() }) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const dst = new PassThrough() dst.pause() diff --git a/test/client-timeout.js b/test/client-timeout.js index 23d0a1ee831..a2c18cd8fd0 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -7,7 +7,6 @@ const { createServer } = require('node:http') const { Readable } = require('node:stream') const FakeTimers = require('@sinonjs/fake-timers') const timers = require('../lib/util/timers') -const { guardDisconnect } = require('./guard-disconnect') test('refresh timeout on pause', async (t) => { t = tspl(t, { plan: 1 }) @@ -181,7 +180,11 @@ test('parser resume with no body timeout', async (t) => { }) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.dispatch({ path: '/', diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index 02793fe6a93..abfe204db74 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -6,7 +6,6 @@ const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -const { guardDisconnect } = require('./guard-disconnect') test('socket close listener does not leak', async (t) => { t = tspl(t, { plan: 32 }) @@ -49,7 +48,11 @@ test('socket close listener does not leak', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (let n = 0; n < 16; ++n) { client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) diff --git a/test/connect-pre-shared-session.js b/test/connect-pre-shared-session.js index e1c5f8fc76f..5bc811ff7c5 100644 --- a/test/connect-pre-shared-session.js +++ b/test/connect-pre-shared-session.js @@ -6,7 +6,6 @@ const { Client } = require('..') const { createServer } = require('node:https') const pem = require('@metcoder95/https-pem') const tls = require('node:tls') -const { guardDisconnect } = require('./guard-disconnect') test('custom session passed to client will be used in tls connect call', async (t) => { t = tspl(t, { plan: 6 }) @@ -32,7 +31,11 @@ test('custom session passed to client will be used in tls connect call', async ( }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const { statusCode, headers, body } = await client.request({ path: '/', diff --git a/test/content-length.js b/test/content-length.js index a03cbd793d8..92386b93b67 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -6,7 +6,6 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { maybeWrapStream, consts } = require('./utils/async-iterators') -const { guardDisconnect } = require('./guard-disconnect') test('request invalid content-length', async (t) => { t = tspl(t, { plan: 7 }) @@ -210,7 +209,11 @@ test('request streaming no body data when content-length=0', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -281,7 +284,11 @@ test('request streaming with Readable.from(buf)', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/guard-disconnect.js b/test/guard-disconnect.js deleted file mode 100644 index 94bbff8e8a4..00000000000 --- a/test/guard-disconnect.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict' - -const { describe, test } = require('node:test') -const { EventEmitter } = require('node:events') -const { strictEqual } = require('node:assert') - -function guardDisconnect (dispatcher, t) { - dispatcher.on('disconnect', () => { - if (!dispatcher.closed && !dispatcher.destroyed) { - t.fail('unexpected disconnect') - } - }) -} - -describe('guardDisconnect', () => { - const cases = [ - { closed: false, destroyed: false, shouldFail: true, label: 'active dispatcher' }, - { closed: true, destroyed: false, shouldFail: false, label: 'closed dispatcher' }, - { closed: false, destroyed: true, shouldFail: false, label: 'destroyed dispatcher' }, - { closed: true, destroyed: true, shouldFail: false, label: 'closed and destroyed dispatcher' } - ] - - for (const { closed, destroyed, shouldFail, label } of cases) { - test(`${shouldFail ? 'calls' : 'does not call'} t.fail for ${label}`, () => { - const dispatcher = new EventEmitter() - dispatcher.closed = closed - dispatcher.destroyed = destroyed - - let failReason = null - const t = { fail: (reason) => { failReason = reason } } - - guardDisconnect(dispatcher, t) - dispatcher.emit('disconnect') - - strictEqual(failReason, shouldFail ? 'unexpected disconnect' : null) - }) - } -}) - -module.exports = { guardDisconnect } diff --git a/test/headers-as-array.js b/test/headers-as-array.js index edeca411f27..a49ccbf3a70 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -4,7 +4,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') test('handle headers as array', async (t) => { t = tspl(t, { plan: 3 }) @@ -21,7 +20,11 @@ test('handle headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -49,7 +52,11 @@ test('handle multi-valued headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -77,7 +84,11 @@ test('handle headers with array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -105,7 +116,11 @@ test('handle multi-valued headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -127,7 +142,11 @@ test('fail if headers array is odd', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -152,7 +171,11 @@ test('fail if headers is not an object or an array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/headers-crlf.js b/test/headers-crlf.js index 91d3b557fa9..2eedab56db6 100644 --- a/test/headers-crlf.js +++ b/test/headers-crlf.js @@ -5,7 +5,6 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { t = tspl(t, { plan: 1 }) @@ -22,7 +21,11 @@ test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' diff --git a/test/http-100.js b/test/http-100.js index 0b2979e3c9a..96dd48d4de9 100644 --- a/test/http-100.js +++ b/test/http-100.js @@ -6,7 +6,6 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const net = require('node:net') const { once } = require('node:events') -const { guardDisconnect } = require('./guard-disconnect') test('ignore informational response', async (t) => { t = tspl(t, { plan: 2 }) @@ -22,7 +21,11 @@ test('ignore informational response', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -140,7 +143,11 @@ test('1xx response without timeouts', async t => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/https.js b/test/https.js index 16699a2e60d..04b8501be16 100644 --- a/test/https.js +++ b/test/https.js @@ -5,7 +5,6 @@ const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') const pem = require('@metcoder95/https-pem') -const { guardDisconnect } = require('./guard-disconnect') test('https get with tls opts', async (t) => { t = tspl(t, { plan: 6 }) @@ -26,7 +25,11 @@ test('https get with tls opts', async (t) => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) @@ -64,7 +67,11 @@ test('https get with tls opts ip', async (t) => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) diff --git a/test/issue-803.js b/test/issue-803.js index f7e4a047c06..33f59804792 100644 --- a/test/issue-803.js +++ b/test/issue-803.js @@ -5,7 +5,6 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t) => { t = tspl(t, { plan: 2 }) @@ -40,7 +39,11 @@ test('https://github.com/nodejs/undici/issues/803', { timeout: 60000 }, async (t const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/max-headers.js b/test/max-headers.js index 386614895e4..60e5609ea1d 100644 --- a/test/max-headers.js +++ b/test/max-headers.js @@ -5,7 +5,6 @@ const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { once } = require('node:events') -const { guardDisconnect } = require('./guard-disconnect') test('handle a lot of headers', async (t) => { t = tspl(t, { plan: 3 }) @@ -26,7 +25,11 @@ test('handle a lot of headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/max-response-size.js b/test/max-response-size.js index aec327c427d..643a76cc031 100644 --- a/test/max-response-size.js +++ b/test/max-response-size.js @@ -4,7 +4,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after, describe } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') describe('max response size', async (t) => { test('default max default size should allow all responses', async (t) => { @@ -24,7 +23,11 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) @@ -59,7 +62,11 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index 8720bd9fc7a..ecc9acaaf0c 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -7,7 +7,6 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -const { guardDisconnect } = require('./guard-disconnect') describe('strictContentLength: false', () => { const emitWarningOriginal = process.emitWarning @@ -164,7 +163,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -206,7 +209,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -248,7 +255,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -290,7 +301,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -332,7 +347,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -373,7 +392,11 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/parser-issues.js b/test/parser-issues.js index 97031c19f55..859a5f096cb 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -4,7 +4,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const net = require('node:net') const { Client, errors } = require('..') -const { guardDisconnect } = require('./guard-disconnect') test('https://github.com/mcollina/undici/issues/268', async (t) => { t = tspl(t, { plan: 2 }) @@ -27,7 +26,11 @@ test('https://github.com/mcollina/undici/issues/268', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ method: 'GET', diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index 7af892fe32c..96452bfaeac 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -6,7 +6,6 @@ const { Client } = require('..') const { createServer } = require('node:http') const { kConnect } = require('../lib/core/symbols') const { kBusy, kPending, kRunning } = require('../lib/core/symbols') -const { guardDisconnect } = require('./guard-disconnect') test('pipeline pipelining', async (t) => { t = tspl(t, { plan: 10 }) @@ -23,7 +22,11 @@ test('pipeline pipelining', async (t) => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client[kConnect](() => { t.equal(client[kRunning], 0) diff --git a/test/promises.js b/test/promises.js index 1d6ba55d596..456a3a6b254 100644 --- a/test/promises.js +++ b/test/promises.js @@ -6,7 +6,6 @@ const { Client, Pool } = require('..') const { createServer } = require('node:http') const { readFileSync, createReadStream } = require('node:fs') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -const { guardDisconnect } = require('./guard-disconnect') test('basic get, async await support', async (t) => { t = tspl(t, { plan: 5 }) @@ -23,7 +22,11 @@ test('basic get, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) @@ -73,7 +76,11 @@ test('basic POST with string, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) @@ -105,7 +112,11 @@ test('basic POST with Buffer, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) @@ -137,7 +148,11 @@ test('basic POST with stream, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, body } = await client.request({ @@ -176,7 +191,11 @@ test('basic POST with async-iterator, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, body } = await client.request({ @@ -238,7 +257,11 @@ test('20 times GET with pipelining 10, async await support', async (t) => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (let i = 0; i < num; i++) { makeRequest(i) @@ -288,7 +311,11 @@ test('pool, async await support', async (t) => { const client = new Pool(`http://localhost:${server.address().port}`) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) diff --git a/test/proxy.js b/test/proxy.js index d530c3a789f..9d209b6be10 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -5,7 +5,6 @@ const { test } = require('node:test') const { Client, Pool } = require('..') const { createServer } = require('node:http') const { createProxy } = require('proxy') -const { guardDisconnect } = require('./guard-disconnect') test('connect through proxy', async (t) => { t = tspl(t, { plan: 3 }) @@ -24,7 +23,11 @@ test('connect through proxy', async (t) => { const client = new Client(proxyUrl) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const response = await client.request({ method: 'GET', @@ -65,7 +68,11 @@ test('connect through proxy with auth', async (t) => { const client = new Client(proxyUrl) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const response = await client.request({ method: 'GET', @@ -107,7 +114,11 @@ test('connect through proxy with auth but invalid credentials', async (t) => { const client = new Client(proxyUrl) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const response = await client.request({ method: 'GET', @@ -141,7 +152,11 @@ test('connect through proxy (with pool)', async (t) => { const pool = new Pool(proxyUrl) - guardDisconnect(pool, t) + pool.on('disconnect', () => { + if (!pool.closed && !pool.destroyed) { + t.fail('unexpected disconnect') + } + }) const response = await pool.request({ method: 'GET', diff --git a/test/request-timeout2.js b/test/request-timeout2.js index 5772a80d963..e58f2f84264 100644 --- a/test/request-timeout2.js +++ b/test/request-timeout2.js @@ -6,7 +6,6 @@ const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -const { guardDisconnect } = require('./guard-disconnect') test('request timeout with slow readable body', async (t) => { t = tspl(t, { plan: 1 }) @@ -26,7 +25,11 @@ test('request timeout with slow readable body', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const body = new Readable({ read () { diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index 0a2fabe9261..25899123ab5 100644 --- a/test/socket-back-pressure.js +++ b/test/socket-back-pressure.js @@ -6,7 +6,6 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { test, after } = require('node:test') -const { guardDisconnect } = require('./guard-disconnect') test('socket back-pressure', async (t) => { t = tspl(t, { plan: 3 }) @@ -38,7 +37,11 @@ test('socket back-pressure', async (t) => { }) after(() => client.close()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => { t.ifError(err) diff --git a/test/stream-compat.js b/test/stream-compat.js index c3aac381197..9b83b70db59 100644 --- a/test/stream-compat.js +++ b/test/stream-compat.js @@ -6,7 +6,6 @@ const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const EE = require('node:events') -const { guardDisconnect } = require('./guard-disconnect') test('stream body without destroy', async (t) => { t = tspl(t, { plan: 2 }) @@ -51,7 +50,11 @@ test('IncomingMessage', async (t) => { const proxyClient = new Client(`http://localhost:${server.address().port}`) after(() => proxyClient.destroy()) - guardDisconnect(proxyClient, t) + proxyClient.on('disconnect', () => { + if (!proxyClient.closed && !proxyClient.destroyed) { + t.fail('unexpected disconnect') + } + }) const proxy = createServer({ joinDuplicateHeaders: true }, (req, res) => { proxyClient.request({ @@ -69,7 +72,11 @@ test('IncomingMessage', async (t) => { const client = new Client(`http://localhost:${proxy.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', diff --git a/test/trailers.js b/test/trailers.js index 150b47c8604..6815632271d 100644 --- a/test/trailers.js +++ b/test/trailers.js @@ -4,7 +4,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') -const { guardDisconnect } = require('./guard-disconnect') test('response trailers missing is OK', async (t) => { t = tspl(t, { plan: 1 }) @@ -20,7 +19,11 @@ test('response trailers missing is OK', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const { body } = await client.request({ path: '/', @@ -51,7 +54,11 @@ test('response trailers missing w trailers is OK', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) - guardDisconnect(client, t) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const { body, trailers } = await client.request({ path: '/', From 5cf144d78c7db68cd735175a7501623d96721aac Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 2 Mar 2026 09:49:55 -0600 Subject: [PATCH 10/12] fix(test): isolate content-encoding chain limit tests with per-test servers The chain limit tests ran concurrently (node:test uses Promise.all within a Suite), so a shared server was unsafe: an aborted connection's async RST delivery on macOS could corrupt the server state for a sibling test. Replace the shared before/after server with a setupChainServer(t) helper that creates an isolated server+client pair per test and binds cleanup to t.after, which is scoped to each individual test context. Also removes the keepalive: false no-op (undici documents this flag as a noop outside of browser context). --- test/fetch/encoding.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 41fe4593fe2..8e89b3b1f80 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -99,14 +99,10 @@ describe('content-encoding chain limit', () => { // Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206) const MAX_CONTENT_ENCODINGS = 5 - let server - before(async () => { - server = createServer({ - noDelay: true - }, (req, res) => { + async function setupChainServer (t) { + const server = createServer({ noDelay: true }, (req, res) => { const encodingCount = parseInt(req.headers['x-encoding-count'] || '1', 10) const encodings = Array(encodingCount).fill('identity').join(', ') - res.writeHead(200, { 'Content-Encoding': encodings, 'Content-Type': 'text/plain' @@ -114,20 +110,21 @@ describe('content-encoding chain limit', () => { res.end('test') }) await once(server.listen(0), 'listening') - }) - - after(() => { - server.closeAllConnections?.() - server.close() - }) - - test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { + t.after(async () => { + server.closeAllConnections?.() + server.close() + await once(server, 'close') + }) const client = new Client(`http://localhost:${server.address().port}`) t.after(() => client.close()) + return { server, client } + } + + test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { + const { server, client } = await setupChainServer(t) const response = await fetch(`http://localhost:${server.address().port}`, { dispatcher: client, - keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS) } }) @@ -137,13 +134,11 @@ describe('content-encoding chain limit', () => { }) test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => client.close()) + const { server, client } = await setupChainServer(t) await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { dispatcher: client, - keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS + 1) } }), (err) => { @@ -154,13 +149,11 @@ describe('content-encoding chain limit', () => { }) test('should reject excessive content-encoding chains', async (t) => { - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => client.close()) + const { server, client } = await setupChainServer(t) await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { dispatcher: client, - keepalive: false, headers: { 'x-encoding-count': '100' } }), (err) => { From 02ce816a7001b74806f80158d0e99a81877b5976 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 6 Mar 2026 13:01:32 -0600 Subject: [PATCH 11/12] fix macos test failure by following pattern set in #4496 --- test/fetch/encoding.js | 49 +++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 8e89b3b1f80..c3d6d542ed7 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -10,7 +10,6 @@ describe('content-encoding handling', () => { const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64') let server - let client before(async () => { server = createServer({ noDelay: true @@ -48,19 +47,16 @@ describe('content-encoding handling', () => { } }) await once(server.listen(0), 'listening') - client = new Client(`http://localhost:${server.address().port}`) }) - after(async () => { - await client.close() + after(() => { server.closeAllConnections?.() server.close() - await once(server, 'close') }) test('content-encoding header', async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - dispatcher: client, + keepalive: false, headers: { 'accept-encoding': 'deflate, gzip' } }) @@ -71,7 +67,7 @@ describe('content-encoding handling', () => { test('content-encoding header is case-iNsENsITIve', async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - dispatcher: client, + keepalive: false, headers: { 'accept-encoding': 'DeFlAtE, GzIp' } }) @@ -84,7 +80,7 @@ describe('content-encoding handling', () => { { skip: typeof require('node:zlib').createZstdDecompress !== 'function' }, async (t) => { const response = await fetch(`http://localhost:${server.address().port}`, { - dispatcher: client, + keepalive: false, headers: { 'accept-encoding': 'zstd' } }) @@ -99,32 +95,37 @@ describe('content-encoding chain limit', () => { // Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206) const MAX_CONTENT_ENCODINGS = 5 - async function setupChainServer (t) { - const server = createServer({ noDelay: true }, (req, res) => { + let server + before(async () => { + server = createServer({ + noDelay: true + }, (req, res) => { + res.socket.setNoDelay(true) const encodingCount = parseInt(req.headers['x-encoding-count'] || '1', 10) const encodings = Array(encodingCount).fill('identity').join(', ') + res.writeHead(200, { 'Content-Encoding': encodings, 'Content-Type': 'text/plain' }) + res.flushHeaders() res.end('test') }) await once(server.listen(0), 'listening') - t.after(async () => { - server.closeAllConnections?.() - server.close() - await once(server, 'close') - }) - const client = new Client(`http://localhost:${server.address().port}`) - t.after(() => client.close()) - return { server, client } - } + }) + + after(() => { + server.closeAllConnections?.() + server.close() + }) test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { - const { server, client } = await setupChainServer(t) + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => client.close()) const response = await fetch(`http://localhost:${server.address().port}`, { dispatcher: client, + keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS) } }) @@ -134,11 +135,13 @@ describe('content-encoding chain limit', () => { }) test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { - const { server, client } = await setupChainServer(t) + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => client.close()) await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { dispatcher: client, + keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS + 1) } }), (err) => { @@ -149,11 +152,13 @@ describe('content-encoding chain limit', () => { }) test('should reject excessive content-encoding chains', async (t) => { - const { server, client } = await setupChainServer(t) + const client = new Client(`http://localhost:${server.address().port}`) + t.after(() => client.close()) await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { dispatcher: client, + keepalive: false, headers: { 'x-encoding-count': '100' } }), (err) => { From ba79c89afbbe122d5cb2544f158ab784b5ce91f8 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 6 Mar 2026 13:37:01 -0600 Subject: [PATCH 12/12] Specify host in CI --- test/fetch/encoding.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index c3d6d542ed7..4f6c16ed687 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -111,7 +111,7 @@ describe('content-encoding chain limit', () => { res.flushHeaders() res.end('test') }) - await once(server.listen(0), 'listening') + await once(server.listen(0, '127.0.0.1'), 'listening') }) after(() => { @@ -120,10 +120,10 @@ describe('content-encoding chain limit', () => { }) test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { - const client = new Client(`http://localhost:${server.address().port}`) + const client = new Client(`http://127.0.0.1:${server.address().port}`) t.after(() => client.close()) - const response = await fetch(`http://localhost:${server.address().port}`, { + const response = await fetch(`http://127.0.0.1:${server.address().port}`, { dispatcher: client, keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS) } @@ -135,11 +135,11 @@ describe('content-encoding chain limit', () => { }) test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { - const client = new Client(`http://localhost:${server.address().port}`) + const client = new Client(`http://127.0.0.1:${server.address().port}`) t.after(() => client.close()) await t.assert.rejects( - fetch(`http://localhost:${server.address().port}`, { + fetch(`http://127.0.0.1:${server.address().port}`, { dispatcher: client, keepalive: false, headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS + 1) } @@ -152,11 +152,11 @@ describe('content-encoding chain limit', () => { }) test('should reject excessive content-encoding chains', async (t) => { - const client = new Client(`http://localhost:${server.address().port}`) + const client = new Client(`http://127.0.0.1:${server.address().port}`) t.after(() => client.close()) await t.assert.rejects( - fetch(`http://localhost:${server.address().port}`, { + fetch(`http://127.0.0.1:${server.address().port}`, { dispatcher: client, keepalive: false, headers: { 'x-encoding-count': '100' }