diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 57549de6357..f1c696564a0 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -18,3 +18,40 @@ 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, 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 }) + + const client = new Client('http://localhost:3000') + after(() => client.close()) + + guardDisconnect(client, t) + + // ... test logic ... +}) +``` + +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: + +- 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`) 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 26cf21913c2..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,6 +181,8 @@ test('parser resume with no body timeout', async (t) => { }) after(() => client.destroy()) + guardDisconnect(client, t) + client.dispatch({ path: '/', method: 'GET' diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index 09ab13cafb8..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,6 +49,8 @@ test('socket close listener does not leak', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + 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 30031048da7..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,6 +32,8 @@ test('custom session passed to client will be used in tls connect call', async ( }) after(() => client.close()) + guardDisconnect(client, t) + const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' diff --git a/test/content-length.js b/test/content-length.js index 6d8e7799c91..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,6 +210,8 @@ 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.request({ path: '/', method: 'PUT', @@ -278,6 +281,8 @@ 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.request({ path: '/', method: 'PUT', 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' } }) 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 9fd4e738af8..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,6 +21,8 @@ test('handle headers as array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET', @@ -46,6 +49,8 @@ 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.request({ path: '/', method: 'GET', @@ -72,6 +77,8 @@ test('handle headers with array', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET', @@ -98,6 +105,8 @@ test('handle multi-valued headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET', @@ -118,6 +127,8 @@ 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.request({ path: '/', method: 'GET', @@ -141,6 +152,8 @@ 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.request({ path: '/', method: 'GET', diff --git a/test/headers-crlf.js b/test/headers-crlf.js index ee440e06045..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,6 +22,8 @@ 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) + const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' try { diff --git a/test/http-100.js b/test/http-100.js index 055a01faaa5..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,6 +22,8 @@ test('ignore informational response', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'POST', @@ -137,6 +140,8 @@ test('1xx response without timeouts', async t => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'POST', diff --git a/test/https.js b/test/https.js index dd5078bd2d8..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,6 +26,8 @@ test('https get with tls opts', async (t) => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) @@ -61,6 +64,8 @@ test('https get with tls opts ip', async (t) => { }) after(() => client.close()) + guardDisconnect(client, t) + 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..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,6 +40,8 @@ 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.request({ path: '/', method: 'GET' diff --git a/test/max-headers.js b/test/max-headers.js index 9edd8d1efdc..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,6 +26,8 @@ test('handle a lot of headers', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET' diff --git a/test/max-response-size.js b/test/max-response-size.js index c1b39c98204..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,6 +24,8 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { t.ifError(err) t.strictEqual(statusCode, 200) @@ -56,6 +59,8 @@ describe('max response size', async (t) => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 }) after(() => client.close()) + guardDisconnect(client, t) + 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..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,6 +164,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', @@ -203,6 +206,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', @@ -243,6 +248,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', @@ -283,6 +290,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', @@ -323,6 +332,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', @@ -362,6 +373,8 @@ describe('strictContentLength: false', () => { }) after(() => client.close()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', diff --git a/test/parser-issues.js b/test/parser-issues.js index 2d9f04628de..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,6 +27,8 @@ 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.request({ method: 'GET', path: '/nxt/_changes?feed=continuous&heartbeat=5000', diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index bb4cf22e84f..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,6 +23,8 @@ test('pipeline pipelining', async (t) => { }) after(() => client.close()) + guardDisconnect(client, t) + client[kConnect](() => { t.equal(client[kRunning], 0) client.pipeline({ diff --git a/test/promises.js b/test/promises.js index 3aaf909cd71..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,6 +23,8 @@ test('basic get, async await support', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) t.strictEqual(statusCode, 200) @@ -70,6 +73,8 @@ 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) + try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) t.strictEqual(statusCode, 200) @@ -100,6 +105,8 @@ 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) + try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) t.strictEqual(statusCode, 200) @@ -130,6 +137,8 @@ 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) + try { const { statusCode, body } = await client.request({ path: '/', @@ -167,6 +176,8 @@ 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) + try { const { statusCode, body } = await client.request({ path: '/', @@ -227,6 +238,8 @@ test('20 times GET with pipelining 10, async await support', async (t) => { }) after(() => client.close()) + guardDisconnect(client, t) + for (let i = 0; i < num; i++) { makeRequest(i) } @@ -275,6 +288,8 @@ test('pool, async await support', async (t) => { const client = new Pool(`http://localhost:${server.address().port}`) after(() => client.close()) + guardDisconnect(client, t) + 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..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,6 +24,8 @@ test('connect through proxy', async (t) => { const client = new Client(proxyUrl) + guardDisconnect(client, t) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar' @@ -62,6 +65,8 @@ test('connect through proxy with auth', async (t) => { const client = new Client(proxyUrl) + guardDisconnect(client, t) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar', @@ -102,6 +107,8 @@ test('connect through proxy with auth but invalid credentials', async (t) => { const client = new Client(proxyUrl) + guardDisconnect(client, t) + const response = await client.request({ method: 'GET', path: serverUrl + '/hello?foo=bar', @@ -134,6 +141,8 @@ test('connect through proxy (with pool)', async (t) => { const pool = new Pool(proxyUrl) + guardDisconnect(pool, t) + const response = await pool.request({ method: 'GET', path: serverUrl + '/hello?foo=bar' diff --git a/test/request-timeout2.js b/test/request-timeout2.js index 48293f81e95..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,6 +26,8 @@ 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) + const body = new Readable({ read () { if (this._reading) { diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index e7150c0a9c5..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,6 +38,8 @@ test('socket back-pressure', async (t) => { }) after(() => client.close()) + guardDisconnect(client, t) + 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..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,6 +51,8 @@ test('IncomingMessage', async (t) => { const proxyClient = new Client(`http://localhost:${server.address().port}`) after(() => proxyClient.destroy()) + guardDisconnect(proxyClient, t) + const proxy = createServer({ joinDuplicateHeaders: true }, (req, res) => { proxyClient.request({ path: '/', @@ -66,6 +69,8 @@ test('IncomingMessage', async (t) => { const client = new Client(`http://localhost:${proxy.address().port}`) after(() => client.destroy()) + guardDisconnect(client, t) + client.request({ path: '/', method: 'PUT', diff --git a/test/trailers.js b/test/trailers.js index 64a29da7b22..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 }) @@ -18,6 +19,9 @@ test('response trailers missing is OK', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + + guardDisconnect(client, t) + const { body } = await client.request({ path: '/', method: 'GET', @@ -46,6 +50,9 @@ 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()) + + guardDisconnect(client, t) + const { body, trailers } = await client.request({ path: '/', method: 'GET',