From f430b24b645aaaa8d1f0e86d0d11080db43c5130 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 8 Feb 2024 09:22:00 +0100 Subject: [PATCH 001/123] test: remove t.diagnostics() call in push-dont-push.js test (#2715) --- test/fetch/pull-dont-push.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/fetch/pull-dont-push.js b/test/fetch/pull-dont-push.js index 3d4f80d3369..5dbf331f397 100644 --- a/test/fetch/pull-dont-push.js +++ b/test/fetch/pull-dont-push.js @@ -36,9 +36,7 @@ test('pull dont\'t push', async (t) => { server.listen(0) await once(server, 'listening') - t.diagnostic('server listening on port %d', server.address().port) const res = await fetch(`http://localhost:${server.address().port}`) - t.diagnostic('fetched') // Some time is needed to fill the buffer await sleep(1000) From 7308f530bfef595217c6aab1432b557d8a63a57a Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 8 Feb 2024 09:34:48 +0100 Subject: [PATCH 002/123] fix: fix flaky debug test (#2714) --- test/node-test/debug.js | 44 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/test/node-test/debug.js b/test/node-test/debug.js index 276e8d3613d..055b2a121df 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -9,7 +9,7 @@ const { tspl } = require('@matteo.collina/tspl') const removeEscapeColorsRE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g test('debug#websocket', async t => { - const assert = tspl(t, { plan: 6 }) + const assert = tspl(t, { plan: 8 }) const child = spawn( process.execPath, [join(__dirname, '../fixtures/websocket.js')], @@ -24,10 +24,12 @@ test('debug#websocket', async t => { /(WEBSOCKET [0-9]+:) (connecting to)/, // Skip the chunk that comes with the experimental warning /(\[UNDICI-WS\])/, + /\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)/, /(WEBSOCKET [0-9]+:) (connected to)/, /(WEBSOCKET [0-9]+:) (sending request)/, /(WEBSOCKET [0-9]+:) (connection opened)/, - /(WEBSOCKET [0-9]+:) (closed connection to)/ + /(WEBSOCKET [0-9]+:) (closed connection to)/, + /^$/ ] child.stderr.setEncoding('utf8') @@ -35,9 +37,10 @@ test('debug#websocket', async t => { chunks.push(chunk) }) child.stderr.on('end', () => { - assert.strictEqual(chunks.length, assertions.length, JSON.stringify(chunks)) - for (let i = 1; i < chunks.length; i++) { - assert.match(chunks[i].replace(removeEscapeColorsRE, ''), assertions[i]) + const lines = extractLines(chunks) + assert.strictEqual(lines.length, assertions.length) + for (let i = 1; i < lines.length; i++) { + assert.match(lines[i], assertions[i]) } }) @@ -46,7 +49,7 @@ test('debug#websocket', async t => { }) test('debug#fetch', async t => { - const assert = tspl(t, { plan: 6 }) + const assert = tspl(t, { plan: 7 }) const child = spawn( process.execPath, [join(__dirname, '../fixtures/fetch.js')], @@ -60,7 +63,8 @@ test('debug#fetch', async t => { /(FETCH [0-9]+:) (connected to)/, /(FETCH [0-9]+:) (sending request)/, /(FETCH [0-9]+:) (received response)/, - /(FETCH [0-9]+:) (trailers received)/ + /(FETCH [0-9]+:) (trailers received)/, + /^$/ ] child.stderr.setEncoding('utf8') @@ -68,9 +72,10 @@ test('debug#fetch', async t => { chunks.push(chunk) }) child.stderr.on('end', () => { - assert.strictEqual(chunks.length, assertions.length, JSON.stringify(chunks)) - for (let i = 0; i < chunks.length; i++) { - assert.match(chunks[i].replace(removeEscapeColorsRE, ''), assertions[i]) + const lines = extractLines(chunks) + assert.strictEqual(lines.length, assertions.length) + for (let i = 0; i < lines.length; i++) { + assert.match(lines[i], assertions[i]) } }) @@ -80,7 +85,7 @@ test('debug#fetch', async t => { test('debug#undici', async t => { // Due to Node.js webpage redirect - const assert = tspl(t, { plan: 6 }) + const assert = tspl(t, { plan: 7 }) const child = spawn( process.execPath, [join(__dirname, '../fixtures/undici.js')], @@ -96,7 +101,8 @@ test('debug#undici', async t => { /(UNDICI [0-9]+:) (connected to)/, /(UNDICI [0-9]+:) (sending request)/, /(UNDICI [0-9]+:) (received response)/, - /(UNDICI [0-9]+:) (trailers received)/ + /(UNDICI [0-9]+:) (trailers received)/, + /^$/ ] child.stderr.setEncoding('utf8') @@ -104,12 +110,20 @@ test('debug#undici', async t => { chunks.push(chunk) }) child.stderr.on('end', () => { - assert.strictEqual(chunks.length, assertions.length, JSON.stringify(chunks)) - for (let i = 0; i < chunks.length; i++) { - assert.match(chunks[i].replace(removeEscapeColorsRE, ''), assertions[i]) + const lines = extractLines(chunks) + assert.strictEqual(lines.length, assertions.length) + for (let i = 0; i < lines.length; i++) { + assert.match(lines[i], assertions[i]) } }) await assert.completed child.kill() }) + +function extractLines (chunks) { + return chunks + .join('') + .split('\n') + .map(v => v.replace(removeEscapeColorsRE, '')) +} From fee245c942e54234af0523c51a54f24b9178bd9f Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Thu, 8 Feb 2024 12:13:15 +0100 Subject: [PATCH 003/123] fix: HTTP2 tweaks (#2711) Co-authored-by: Khafra --- lib/client.js | 23 +++++++++++++++++++++-- test/http2.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/client.js b/lib/client.js index ff23b772909..98adffdf182 100644 --- a/lib/client.js +++ b/lib/client.js @@ -808,6 +808,7 @@ class Parser { .removeListener('close', onSocketClose) client[kSocket] = null + client[kHTTP2Session] = null client[kQueue][client[kRunningIdx]++] = null client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) @@ -1421,7 +1422,7 @@ function _resume (client, sync) { return } - if (!socket && !client[kHTTP2Session]) { + if (!socket) { connect(client) return } @@ -1796,7 +1797,25 @@ function writeH2 (client, session, request) { }) stream.once('end', () => { - request.onComplete([]) + // When state is null, it means we haven't consumed body and the stream still do not have + // a state. + // Present specially when using pipeline or stream + if (stream.state?.state == null || stream.state.state < 6) { + request.onComplete([]) + return + } + + // Stream is closed or half-closed-remote (6), decrement counter and cleanup + // It does not have sense to continue working with the stream as we do not + // have yet RST_STREAM support on client-side + h2State.openStreams -= 1 + if (h2State.openStreams === 0) { + session.unref() + } + + const err = new InformationalError('HTTP/2: stream half-closed (remote)') + errorRequest(client, request, err) + util.destroy(stream, err) }) stream.on('data', (chunk) => { diff --git a/test/http2.js b/test/http2.js index 7a183a24552..2daef09bd2c 100644 --- a/test/http2.js +++ b/test/http2.js @@ -13,7 +13,7 @@ const { Client, Agent } = require('..') const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20 -plan(24) +plan(25) test('Should support H2 connection', async t => { const body = [] @@ -1243,3 +1243,33 @@ test('The h2 pseudo-headers is not included in the headers', async t => { t.equal(response.statusCode, 200) t.equal(response.headers[':status'], undefined) }) + +test('Should throw informational error on half-closed streams (remote)', async t => { + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + stream.destroy() + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(2) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + await client.request({ + path: '/', + method: 'GET' + }).catch(err => { + t.equal(err.message, 'HTTP/2: stream half-closed (remote)') + t.equal(err.code, 'UND_ERR_INFO') + }) +}) From 14e6a8f6751e18ee9e3d2e920f3f6d2ae6d8735d Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 8 Feb 2024 12:24:30 +0100 Subject: [PATCH 004/123] test: compare with specific errors (#2693) --- test/cookie/cookies.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/cookie/cookies.js b/test/cookie/cookies.js index d6fed72be16..4185559466b 100644 --- a/test/cookie/cookies.js +++ b/test/cookie/cookies.js @@ -82,7 +82,7 @@ test('Cookie Name Validation', () => { maxAge: 3 }) }, - Error + new Error('Invalid cookie name') ) }) }) @@ -116,7 +116,7 @@ test('Cookie Value Validation', () => { } ) }, - Error, + new Error('Invalid header value'), "RFC2616 cookie 'Space'" ) }) @@ -128,7 +128,7 @@ test('Cookie Value Validation', () => { value: 'United Kingdom' }) }, - Error, + new Error('Invalid header value'), "RFC2616 cookie 'location' cannot contain character ' '" ) }) @@ -147,7 +147,7 @@ test('Cookie Path Validation', () => { maxAge: 3 }) }, - Error, + new Error('Invalid cookie path'), path + ": Invalid cookie path char ';'" ) }) @@ -167,7 +167,7 @@ test('Cookie Domain Validation', () => { maxAge: 3 }) }, - Error, + new Error('Invalid cookie domain'), 'Invalid first/last char in cookie domain: ' + domain ) }) From 16615408157040409bf317d6af1e467a7f5d0c02 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 8 Feb 2024 14:57:28 +0100 Subject: [PATCH 005/123] test: response.url after redirect is set to target url (#2716) --- test/fetch/fetch-url-after-redirect.js | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/fetch/fetch-url-after-redirect.js diff --git a/test/fetch/fetch-url-after-redirect.js b/test/fetch/fetch-url-after-redirect.js new file mode 100644 index 00000000000..ecc112eb7c1 --- /dev/null +++ b/test/fetch/fetch-url-after-redirect.js @@ -0,0 +1,40 @@ +'use strict' + +const assert = require('node:assert') +const { test } = require('node:test') +const { createServer } = require('node:http') +const { fetch } = require('../..') +const { closeServerAsPromise } = require('../utils/node-http') +const { promisify } = require('node:util') + +test('after redirecting the url of the response is set to the target url', async (t) => { + // redirect-1 -> redirect-2 -> target + const server = createServer((req, res) => { + switch (res.req.url) { + case '/redirect-1': + res.writeHead(302, undefined, { Location: '/redirect-2' }) + res.end() + break + case '/redirect-2': + res.writeHead(302, undefined, { Location: '/redirect-3' }) + res.end() + break + case '/redirect-3': + res.writeHead(302, undefined, { Location: '/target' }) + res.end() + break + case '/target': + res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' }) + res.end() + break + } + }) + t.after(closeServerAsPromise(server)) + + const listenAsync = promisify(server.listen.bind(server)) + await listenAsync(0) + const { port } = server.address() + const response = await fetch(`http://127.0.0.1:${port}/redirect-1`) + + assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`) +}) From 0a069ab1f2d111b8e74f2d444d7f5678e302f39d Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 8 Feb 2024 16:58:03 +0100 Subject: [PATCH 006/123] chore: remove mocha and chai (#2696) * chore: remove mocha and chai, fix bug in fetch request * fix test * uncomment test cases * remove p-timeout * fix * add back removed line --- package.json | 20 +- test/node-fetch/headers.js | 185 +++--- test/node-fetch/main.js | 904 +++++++++++++------------- test/node-fetch/mock.js | 28 +- test/node-fetch/request.js | 160 +++-- test/node-fetch/response.js | 182 +++--- test/node-fetch/utils/chai-timeout.js | 15 - 7 files changed, 739 insertions(+), 755 deletions(-) delete mode 100644 test/node-fetch/utils/chai-timeout.js diff --git a/package.json b/package.json index a52222ce200..1ce1a058b1b 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "lint:fix": "standard --fix | snazzy", "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test", "test:cookies": "borp --coverage -p \"test/cookie/*.js\"", - "test:node-fetch": "mocha --exit test/node-fetch", + "test:node-fetch": "borp --coverage -p \"test/node-fetch/**/*.js\"", "test:eventsource": "npm run build:node && borp --expose-gc --coverage -p \"test/eventsource/*.js\"", "test:fetch": "npm run build:node && borp --expose-gc --coverage -p \"test/fetch/*.js\" && borp --coverage -p \"test/webidl/*.js\"", "test:jest": "jest", @@ -104,17 +104,15 @@ "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", "abort-controller": "^3.0.0", + "axios": "^1.6.5", "borp": "^0.9.1", - "chai": "^4.3.4", - "chai-as-promised": "^7.1.1", - "chai-iterator": "^3.0.2", - "chai-string": "^1.5.0", "concurrently": "^8.0.1", "cronometro": "^2.0.2", "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", "form-data": "^4.0.0", "formdata-node": "^6.0.3", + "got": "^14.0.0", "https-pem": "^3.0.0", "husky": "^9.0.7", "import-fresh": "^3.3.0", @@ -122,11 +120,11 @@ "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", "mitata": "^0.1.8", - "mocha": "^10.0.0", - "p-timeout": "^3.2.0", + "node-fetch": "^3.3.2", "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.1.3", + "request": "^2.88.2", "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", @@ -134,18 +132,14 @@ "tsd": "^0.30.1", "typescript": "^5.0.2", "wait-on": "^7.0.1", - "ws": "^8.11.0", - "axios": "^1.6.5", - "got": "^14.0.0", - "node-fetch": "^3.3.2", - "request": "^2.88.2" + "ws": "^8.11.0" }, "engines": { "node": ">=18.0" }, "standard": { "env": [ - "mocha" + "jest" ], "ignore": [ "lib/llhttp/constants.js", diff --git a/test/node-fetch/headers.js b/test/node-fetch/headers.js index 2001e976efa..8500818f354 100644 --- a/test/node-fetch/headers.js +++ b/test/node-fetch/headers.js @@ -1,18 +1,14 @@ -/* eslint no-unused-expressions: "off" */ +'use strict' +const assert = require('node:assert') +const { describe, it } = require('node:test') const { format } = require('node:util') -const chai = require('chai') -const chaiIterator = require('chai-iterator') -const { Headers } = require('../../lib/fetch/headers.js') - -chai.use(chaiIterator) - -const { expect } = chai +const { Headers } = require('../../index.js') describe('Headers', () => { it('should have attributes conforming to Web IDL', () => { const headers = new Headers() - expect(Object.getOwnPropertyNames(headers)).to.be.empty + assert.strictEqual(Object.getOwnPropertyNames(headers).length, 0) const enumerableProperties = [] for (const property in headers) { @@ -30,7 +26,7 @@ describe('Headers', () => { 'set', 'values' ]) { - expect(enumerableProperties).to.contain(toCheck) + assert.strictEqual(enumerableProperties.includes(toCheck), true) } }) @@ -41,14 +37,14 @@ describe('Headers', () => { ['b', '3'], ['a', '1'] ]) - expect(headers).to.have.property('forEach') + assert.strictEqual(typeof headers.forEach, 'function') const result = [] for (const [key, value] of headers.entries()) { result.push([key, value]) } - expect(result).to.deep.equal([ + assert.deepStrictEqual(result, [ ['a', '1'], ['b', '2, 3'], ['c', '4'] @@ -66,15 +62,15 @@ describe('Headers', () => { results.push({ value, key, object }) }) - expect(results.length).to.equal(2) - expect({ key: 'accept', value: 'application/json, text/plain', object: headers }).to.deep.equal(results[0]) - expect({ key: 'content-type', value: 'text/html', object: headers }).to.deep.equal(results[1]) + assert.strictEqual(results.length, 2) + assert.deepStrictEqual(results[0], { key: 'accept', value: 'application/json, text/plain', object: headers }) + assert.deepStrictEqual(results[1], { key: 'content-type', value: 'text/html', object: headers }) }) - xit('should set "this" to undefined by default on forEach', () => { + it.skip('should set "this" to undefined by default on forEach', () => { const headers = new Headers({ Accept: 'application/json' }) headers.forEach(function () { - expect(this).to.be.undefined + assert.strictEqual(this, undefined) }) }) @@ -82,7 +78,7 @@ describe('Headers', () => { const headers = new Headers({ Accept: 'application/json' }) const thisArg = {} headers.forEach(function () { - expect(this).to.equal(thisArg) + assert.strictEqual(this, thisArg) }, thisArg) }) @@ -93,14 +89,14 @@ describe('Headers', () => { ['a', '1'] ]) headers.append('b', '3') - expect(headers).to.be.iterable + assert.strictEqual(typeof headers[Symbol.iterator], 'function') const result = [] for (const pair of headers) { result.push(pair) } - expect(result).to.deep.equal([ + assert.deepStrictEqual(result, [ ['a', '1'], ['b', '2, 3'], ['c', '4'] @@ -115,12 +111,22 @@ describe('Headers', () => { ]) headers.append('b', '3') - expect(headers.entries()).to.be.iterable - .and.to.deep.iterate.over([ - ['a', '1'], - ['b', '2, 3'], - ['c', '4'] - ]) + assert.strictEqual(typeof headers.entries, 'function') + assert.strictEqual(typeof headers.entries()[Symbol.iterator], 'function') + + const entries = headers.entries() + assert.strictEqual(typeof entries.next, 'function') + assert.deepStrictEqual(entries.next().value, ['a', '1']) + assert.strictEqual(typeof entries.next, 'function') + assert.deepStrictEqual(entries.next().value, ['b', '2, 3']) + assert.strictEqual(typeof entries.next, 'function') + assert.deepStrictEqual(entries.next().value, ['c', '4']) + + assert.deepStrictEqual([...headers.entries()], [ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]) }) it('should allow iterating through all headers with keys()', () => { @@ -131,8 +137,18 @@ describe('Headers', () => { ]) headers.append('b', '3') - expect(headers.keys()).to.be.iterable - .and.to.iterate.over(['a', 'b', 'c']) + assert.strictEqual(typeof headers.keys, 'function') + assert.strictEqual(typeof headers.keys()[Symbol.iterator], 'function') + + const keys = headers.keys() + assert.strictEqual(typeof keys.next, 'function') + assert.strictEqual(keys.next().value, 'a') + assert.strictEqual(typeof keys.next, 'function') + assert.strictEqual(keys.next().value, 'b') + assert.strictEqual(typeof keys.next, 'function') + assert.strictEqual(keys.next().value, 'c') + + assert.deepStrictEqual([...headers.keys()], ['a', 'b', 'c']) }) it('should allow iterating through all headers with values()', () => { @@ -143,26 +159,37 @@ describe('Headers', () => { ]) headers.append('b', '3') - expect(headers.values()).to.be.iterable - .and.to.iterate.over(['1', '2, 3', '4']) + assert.strictEqual(typeof headers.values, 'function') + assert.strictEqual(typeof headers.values()[Symbol.iterator], 'function') + + const values = headers.values() + assert.strictEqual(typeof values.next, 'function') + assert.strictEqual(values.next().value, '1') + assert.strictEqual(typeof values.next, 'function') + assert.strictEqual(values.next().value, '2, 3') + assert.strictEqual(typeof values.next, 'function') + assert.strictEqual(values.next().value, '4') + + assert.deepStrictEqual([...headers.values()], ['1', '2, 3', '4']) }) it('should reject illegal header', () => { const headers = new Headers() - expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError) - expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError) - expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError) - expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError) - expect(() => headers.delete('Hé-y')).to.throw(TypeError) - expect(() => headers.get('Hé-y')).to.throw(TypeError) - expect(() => headers.has('Hé-y')).to.throw(TypeError) - expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError) + + assert.throws(() => new Headers({ 'He y': 'ok' }), TypeError) + assert.throws(() => new Headers({ 'Hé-y': 'ok' }), TypeError) + assert.throws(() => new Headers({ 'He-y': 'ăk' }), TypeError) + assert.throws(() => headers.append('Hé-y', 'ok'), TypeError) + assert.throws(() => headers.delete('Hé-y'), TypeError) + assert.throws(() => headers.get('Hé-y'), TypeError) + assert.throws(() => headers.has('Hé-y'), TypeError) + assert.throws(() => headers.set('Hé-y', 'ok'), TypeError) // Should reject empty header - expect(() => headers.append('', 'ok')).to.throw(TypeError) + assert.throws(() => headers.append('', 'ok'), TypeError) }) - xit('should ignore unsupported attributes while reading headers', () => { - const FakeHeader = function () {} + it.skip('should ignore unsupported attributes while reading headers', () => { + const FakeHeader = function () { } // Prototypes are currently ignored // This might change in the future: #181 FakeHeader.prototype.z = 'fake' @@ -188,26 +215,26 @@ describe('Headers', () => { const h1Raw = h1.raw() - expect(h1Raw.a).to.include('string') - expect(h1Raw.b).to.include('1,2') - expect(h1Raw.c).to.include('') - expect(h1Raw.d).to.include('') - expect(h1Raw.e).to.include('1') - expect(h1Raw.f).to.include('1,2') - expect(h1Raw.g).to.include('[object Object]') - expect(h1Raw.h).to.include('undefined') - expect(h1Raw.i).to.include('null') - expect(h1Raw.j).to.include('NaN') - expect(h1Raw.k).to.include('true') - expect(h1Raw.l).to.include('false') - expect(h1Raw.m).to.include('test') - expect(h1Raw.n).to.include('1,2') - expect(h1Raw.n).to.include('3,4') - - expect(h1Raw.z).to.be.undefined + assert.strictEqual(h1Raw.a.includes('string'), true) + assert.strictEqual(h1Raw.b.includes('1,2'), true) + assert.strictEqual(h1Raw.c.includes(''), true) + assert.strictEqual(h1Raw.d.includes(''), true) + assert.strictEqual(h1Raw.e.includes('1'), true) + assert.strictEqual(h1Raw.f.includes('1,2'), true) + assert.strictEqual(h1Raw.g.includes('[object Object]'), true) + assert.strictEqual(h1Raw.h.includes('undefined'), true) + assert.strictEqual(h1Raw.i.includes('null'), true) + assert.strictEqual(h1Raw.j.includes('NaN'), true) + assert.strictEqual(h1Raw.k.includes('true'), true) + assert.strictEqual(h1Raw.l.includes('false'), true) + assert.strictEqual(h1Raw.m.includes('test'), true) + assert.strictEqual(h1Raw.n.includes('1,2'), true) + assert.strictEqual(h1Raw.n.includes('3,4'), true) + + assert.strictEqual(h1Raw.z, undefined) }) - xit('should wrap headers', () => { + it.skip('should wrap headers', () => { const h1 = new Headers({ a: '1' }) @@ -221,16 +248,16 @@ describe('Headers', () => { h3.append('a', '2') const h3Raw = h3.raw() - expect(h1Raw.a).to.include('1') - expect(h1Raw.a).to.not.include('2') + assert.strictEqual(h1Raw.a.includes('1'), true) + assert.strictEqual(h1Raw.a.includes('2'), false) - expect(h2Raw.a).to.include('1') - expect(h2Raw.a).to.not.include('2') - expect(h2Raw.b).to.include('1') + assert.strictEqual(h2Raw.a.includes('1'), true) + assert.strictEqual(h2Raw.a.includes('2'), false) + assert.strictEqual(h2Raw.b.includes('1'), true) - expect(h3Raw.a).to.include('1') - expect(h3Raw.a).to.include('2') - expect(h3Raw.b).to.include('1') + assert.strictEqual(h3Raw.a.includes('1'), true) + assert.strictEqual(h3Raw.a.includes('2'), true) + assert.strictEqual(h3Raw.b.includes('1'), true) }) it('should accept headers as an iterable of tuples', () => { @@ -241,33 +268,33 @@ describe('Headers', () => { ['b', '2'], ['a', '3'] ]) - expect(headers.get('a')).to.equal('1, 3') - expect(headers.get('b')).to.equal('2') + assert.strictEqual(headers.get('a'), '1, 3') + assert.strictEqual(headers.get('b'), '2') headers = new Headers([ new Set(['a', '1']), ['b', '2'], new Map([['a', null], ['3', null]]).keys() ]) - expect(headers.get('a')).to.equal('1, 3') - expect(headers.get('b')).to.equal('2') + assert.strictEqual(headers.get('a'), '1, 3') + assert.strictEqual(headers.get('b'), '2') headers = new Headers(new Map([ ['a', '1'], ['b', '2'] ])) - expect(headers.get('a')).to.equal('1') - expect(headers.get('b')).to.equal('2') + assert.strictEqual(headers.get('a'), '1') + assert.strictEqual(headers.get('b'), '2') }) it('should throw a TypeError if non-tuple exists in a headers initializer', () => { - expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError) - expect(() => new Headers(['b2'])).to.throw(TypeError) - expect(() => new Headers('b2')).to.throw(TypeError) - expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError) + assert.throws(() => new Headers([['b', '2', 'huh?']]), TypeError) + assert.throws(() => new Headers(['b2']), TypeError) + assert.throws(() => new Headers('b2'), TypeError) + assert.throws(() => new Headers({ [Symbol.iterator]: 42 }), TypeError) }) - xit('should use a custom inspect function', () => { + it.skip('should use a custom inspect function', () => { const headers = new Headers([ ['Host', 'thehost'], ['Host', 'notthehost'], @@ -277,6 +304,6 @@ describe('Headers', () => { ]) // eslint-disable-next-line quotes - expect(format(headers)).to.equal("{ a: [ '1', '3' ], b: '2', host: 'thehost' }") + assert.strictEqual(format(headers), "{ a: [ '1', '3' ], b: '2', host: 'thehost' }") }) }) diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index 7e6da3f984f..e4b13b1f482 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -1,17 +1,14 @@ -/* eslint no-unused-expressions: "off" */ -/* globals AbortController */ +'use strict' // Test tools +const assert = require('node:assert') +const { describe, it, before, beforeEach, after } = require('node:test') +const { setTimeout: delay } = require('node:timers/promises') const zlib = require('node:zlib') const stream = require('node:stream') const vm = require('node:vm') -const chai = require('chai') const crypto = require('node:crypto') -const chaiPromised = require('chai-as-promised') -const chaiIterator = require('chai-iterator') -const chaiString = require('chai-string') const { Blob } = require('node:buffer') -const { setTimeout: delay } = require('timers/promises') const { fetch, @@ -23,25 +20,14 @@ const { Agent } = require('../../index.js') const HeadersOrig = require('../../lib/fetch/headers.js').Headers -const RequestOrig = require('../../lib/fetch/request.js').Request const ResponseOrig = require('../../lib/fetch/response.js').Response +const RequestOrig = require('../../lib/fetch/request.js').Request const TestServer = require('./utils/server.js') -const chaiTimeout = require('./utils/chai-timeout.js') - -function isNodeLowerThan (version) { - return !~process.version.localeCompare(version, undefined, { numeric: true }) -} const { Uint8Array: VMUint8Array } = vm.runInNewContext('this') -chai.use(chaiPromised) -chai.use(chaiIterator) -chai.use(chaiString) -chai.use(chaiTimeout) -const { expect } = chai - describe('node-fetch', () => { const local = new TestServer() let base @@ -63,87 +49,86 @@ describe('node-fetch', () => { it('should return a promise', () => { const url = `${base}hello` const p = fetch(url) - expect(p).to.be.an.instanceof(Promise) - expect(p).to.have.property('then') + assert.ok(p instanceof Promise) + assert.strictEqual(typeof p.then, 'function') }) it('should expose Headers, Response and Request constructors', () => { - expect(Headers).to.equal(HeadersOrig) - expect(Response).to.equal(ResponseOrig) - expect(Request).to.equal(RequestOrig) + assert.strictEqual(Headers, HeadersOrig) + assert.strictEqual(Response, ResponseOrig) + assert.strictEqual(Request, RequestOrig) }) it('should support proper toString output for Headers, Response and Request objects', () => { - expect(new Headers().toString()).to.equal('[object Headers]') - expect(new Response().toString()).to.equal('[object Response]') - expect(new Request(base).toString()).to.equal('[object Request]') + assert.strictEqual(new Headers().toString(), '[object Headers]') + assert.strictEqual(new Response().toString(), '[object Response]') + assert.strictEqual(new Request(base).toString(), '[object Request]') }) - + // TODO Should we reflect the input? it('should reject with error if url is protocol relative', () => { const url = '//example.com/' - return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) + return assert.rejects(fetch(url), new TypeError('Failed to parse URL from //example.com/')) }) it('should reject with error if url is relative path', () => { const url = '/some/path' - return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) + return assert.rejects(fetch(url), new TypeError('Failed to parse URL from /some/path')) }) + // TODO: This seems odd it('should reject with error if protocol is unsupported', () => { const url = 'ftp://example.com/' - return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) + return assert.rejects(fetch(url), new TypeError('fetch failed')) }) - it('should reject with error on network failure', function () { - this.timeout(5000) + it('should reject with error on network failure', { timeout: 5000 }, function () { const url = 'http://localhost:50000/' - return expect(fetch(url)).to.eventually.be.rejected - .and.be.an.instanceOf(TypeError) + return assert.rejects(fetch(url), new TypeError('fetch failed')) }) it('should resolve into response', () => { const url = `${base}hello` return fetch(url).then(res => { - expect(res).to.be.an.instanceof(Response) - expect(res.headers).to.be.an.instanceof(Headers) - expect(res.body).to.be.an.instanceof(ReadableStream) - expect(res.bodyUsed).to.be.false + assert.ok(res instanceof Response) + assert.ok(res.headers instanceof Headers) + assert.ok(res.body instanceof ReadableStream) + assert.strictEqual(res.bodyUsed, false) - expect(res.url).to.equal(url) - expect(res.ok).to.be.true - expect(res.status).to.equal(200) - expect(res.statusText).to.equal('OK') + assert.strictEqual(res.url, url) + assert.strictEqual(res.ok, true) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.statusText, 'OK') }) }) it('Response.redirect should resolve into response', () => { const res = Response.redirect('http://localhost') - expect(res).to.be.an.instanceof(Response) - expect(res.headers).to.be.an.instanceof(Headers) - expect(res.headers.get('location')).to.equal('http://localhost/') - expect(res.status).to.equal(302) + assert.ok(res instanceof Response) + assert.ok(res.headers instanceof Headers) + assert.strictEqual(res.headers.get('location'), 'http://localhost/') + assert.strictEqual(res.status, 302) }) it('Response.redirect /w invalid url should fail', () => { - expect(() => { + assert.throws(() => { Response.redirect('localhost') - }).to.throw() + }) }) it('Response.redirect /w invalid status should fail', () => { - expect(() => { + assert.throws(() => { Response.redirect('http://localhost', 200) - }).to.throw() + }) }) it('should accept plain text response', () => { const url = `${base}plain` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(res.bodyUsed).to.be.true - expect(result).to.be.a('string') - expect(result).to.equal('text') + assert.strictEqual(res.bodyUsed, true) + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'text') }) }) }) @@ -151,11 +136,11 @@ describe('node-fetch', () => { it('should accept html response (like plain text)', () => { const url = `${base}html` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/html') + assert.strictEqual(res.headers.get('content-type'), 'text/html') return res.text().then(result => { - expect(res.bodyUsed).to.be.true - expect(result).to.be.a('string') - expect(result).to.equal('') + assert.strictEqual(res.bodyUsed, true) + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) @@ -163,11 +148,11 @@ describe('node-fetch', () => { it('should accept json response', () => { const url = `${base}json` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('application/json') + assert.strictEqual(res.headers.get('content-type'), 'application/json') return res.json().then(result => { - expect(res.bodyUsed).to.be.true - expect(result).to.be.an('object') - expect(result).to.deep.equal({ name: 'value' }) + assert.strictEqual(res.bodyUsed, true) + assert.strictEqual(typeof result, 'object') + assert.deepStrictEqual(result, { name: 'value' }) }) }) }) @@ -180,7 +165,7 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.headers['x-custom-header']).to.equal('abc') + assert.strictEqual(res.headers['x-custom-header'], 'abc') }) }) @@ -192,7 +177,7 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.headers['x-custom-header']).to.equal('abc') + assert.strictEqual(res.headers['x-custom-header'], 'abc') }) }) @@ -204,7 +189,7 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.headers['x-custom-header']).to.equal('abc,123') + assert.strictEqual(res.headers['x-custom-header'], 'abc,123') }) }) @@ -216,56 +201,56 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.headers['x-custom-header']).to.equal('abc') + assert.strictEqual(res.headers['x-custom-header'], 'abc') }) }) it('should follow redirect code 301', () => { const url = `${base}redirect/301` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) - expect(res.ok).to.be.true + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.ok, true) }) }) it('should follow redirect code 302', () => { const url = `${base}redirect/302` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) it('should follow redirect code 303', () => { const url = `${base}redirect/303` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) it('should follow redirect code 307', () => { const url = `${base}redirect/307` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) it('should follow redirect code 308', () => { const url = `${base}redirect/308` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) it('should follow redirect chain', () => { const url = `${base}redirect/chain` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) @@ -276,11 +261,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(result => { - expect(result.method).to.equal('GET') - expect(result.body).to.equal('') + assert.strictEqual(result.method, 'GET') + assert.strictEqual(result.body, '') }) }) }) @@ -292,11 +277,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(res => { - expect(res.method).to.equal('PATCH') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'PATCH') + assert.strictEqual(res.body, 'a=1') }) }) }) @@ -308,11 +293,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(result => { - expect(result.method).to.equal('GET') - expect(result.body).to.equal('') + assert.strictEqual(result.method, 'GET') + assert.strictEqual(result.body, '') }) }) }) @@ -324,11 +309,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(res => { - expect(res.method).to.equal('PATCH') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'PATCH') + assert.strictEqual(res.body, 'a=1') }) }) }) @@ -340,11 +325,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(result => { - expect(result.method).to.equal('GET') - expect(result.body).to.equal('') + assert.strictEqual(result.method, 'GET') + assert.strictEqual(result.body, '') }) }) }) @@ -356,11 +341,11 @@ describe('node-fetch', () => { body: 'a=1' } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) return res.json().then(result => { - expect(result.method).to.equal('PATCH') - expect(result.body).to.equal('a=1') + assert.strictEqual(result.method, 'PATCH') + assert.strictEqual(result.body, 'a=1') }) }) }) @@ -371,21 +356,19 @@ describe('node-fetch', () => { method: 'PATCH', body: stream.Readable.from('tada') } - return expect(fetch(url, options)).to.eventually.be.rejected - .and.be.an.instanceOf(TypeError) + return assert.rejects(fetch(url, options), new TypeError('RequestInit: duplex option is required when sending a body.')) }) it('should obey maximum redirect, reject case', () => { const url = `${base}redirect/chain/20` - return expect(fetch(url)).to.eventually.be.rejected - .and.be.an.instanceOf(TypeError) + return assert.rejects(fetch(url), new TypeError('fetch failed')) }) it('should obey redirect chain, resolve case', () => { const url = `${base}redirect/chain/19` return fetch(url).then(res => { - expect(res.url).to.equal(`${base}inspect`) - expect(res.status).to.equal(200) + assert.strictEqual(res.url, `${base}inspect`) + assert.strictEqual(res.status, 200) }) }) @@ -394,8 +377,7 @@ describe('node-fetch', () => { const options = { redirect: 'error' } - return expect(fetch(url, options)).to.eventually.be.rejected - .and.be.an.instanceOf(TypeError) + return assert.rejects(fetch(url, options), new TypeError('fetch failed')) }) it('should support redirect mode, manual flag when there is no redirect', () => { @@ -404,9 +386,9 @@ describe('node-fetch', () => { redirect: 'manual' } return fetch(url, options).then(res => { - expect(res.url).to.equal(url) - expect(res.status).to.equal(200) - expect(res.headers.get('location')).to.be.null + assert.strictEqual(res.url, url) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.headers.get('location'), null) }) }) @@ -416,19 +398,19 @@ describe('node-fetch', () => { headers: new Headers({ 'x-custom-header': 'abc' }) } return fetch(url, options).then(res => { - expect(res.url).to.equal(`${base}inspect`) + assert.strictEqual(res.url, `${base}inspect`) return res.json() }).then(res => { - expect(res.headers['x-custom-header']).to.equal('abc') + assert.strictEqual(res.headers['x-custom-header'], 'abc') }) }) it('should treat broken redirect as ordinary response (follow)', () => { const url = `${base}redirect/no-location` return fetch(url).then(res => { - expect(res.url).to.equal(url) - expect(res.status).to.equal(301) - expect(res.headers.get('location')).to.be.null + assert.strictEqual(res.url, url) + assert.strictEqual(res.status, 301) + assert.strictEqual(res.headers.get('location'), null) }) }) @@ -438,9 +420,9 @@ describe('node-fetch', () => { redirect: 'manual' } return fetch(url, options).then(res => { - expect(res.url).to.equal(url) - expect(res.status).to.equal(301) - expect(res.headers.get('location')).to.be.null + assert.strictEqual(res.url, url) + assert.strictEqual(res.status, 301) + assert.strictEqual(res.headers.get('location'), null) }) }) @@ -450,37 +432,37 @@ describe('node-fetch', () => { redirect: 'foobar' } return fetch(url, options).then(() => { - expect.fail() + assert.fail() }, error => { - expect(error).to.be.an.instanceOf(TypeError) + assert.ok(error instanceof TypeError) }) }) it('should set redirected property on response when redirect', () => { const url = `${base}redirect/301` return fetch(url).then(res => { - expect(res.redirected).to.be.true + assert.strictEqual(res.redirected, true) }) }) it('should not set redirected property on response without redirect', () => { const url = `${base}hello` return fetch(url).then(res => { - expect(res.redirected).to.be.false + assert.strictEqual(res.redirected, false) }) }) it('should handle client-error response', () => { const url = `${base}error/400` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') - expect(res.status).to.equal(400) - expect(res.statusText).to.equal('Bad Request') - expect(res.ok).to.be.false + assert.strictEqual(res.headers.get('content-type'), 'text/plain') + assert.strictEqual(res.status, 400) + assert.strictEqual(res.statusText, 'Bad Request') + assert.strictEqual(res.ok, false) return res.text().then(result => { - expect(res.bodyUsed).to.be.true - expect(result).to.be.a('string') - expect(result).to.equal('client error') + assert.strictEqual(res.bodyUsed, true) + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'client error') }) }) }) @@ -488,37 +470,37 @@ describe('node-fetch', () => { it('should handle server-error response', () => { const url = `${base}error/500` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') - expect(res.status).to.equal(500) - expect(res.statusText).to.equal('Internal Server Error') - expect(res.ok).to.be.false + assert.strictEqual(res.headers.get('content-type'), 'text/plain') + assert.strictEqual(res.status, 500) + assert.strictEqual(res.statusText, 'Internal Server Error') + assert.strictEqual(res.ok, false) return res.text().then(result => { - expect(res.bodyUsed).to.be.true - expect(result).to.be.a('string') - expect(result).to.equal('server error') + assert.strictEqual(res.bodyUsed, true) + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'server error') }) }) }) it('should handle network-error response', () => { const url = `${base}error/reset` - return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) + return assert.rejects(fetch(url), new TypeError('fetch failed')) }) it('should handle network-error partial response', () => { const url = `${base}error/premature` return fetch(url).then(res => { - expect(res.status).to.equal(200) - expect(res.ok).to.be.true - return expect(res.text()).to.eventually.be.rejectedWith(Error) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.ok, true) + return assert.rejects(() => res.text(), new TypeError('terminated')) }) }) it('should handle network-error in chunked response async iterator', () => { const url = `${base}error/premature/chunked` return fetch(url).then(res => { - expect(res.status).to.equal(200) - expect(res.ok).to.be.true + assert.strictEqual(res.status, 200) + assert.strictEqual(res.ok, true) const read = async body => { const chunks = [] @@ -529,74 +511,75 @@ describe('node-fetch', () => { return chunks } - return expect(read(res.body)) - .to.eventually.be.rejectedWith(Error) + return assert.rejects(read(res.body), new TypeError('terminated')) }) }) it('should handle network-error in chunked response in consumeBody', () => { const url = `${base}error/premature/chunked` return fetch(url).then(res => { - expect(res.status).to.equal(200) - expect(res.ok).to.be.true + assert.strictEqual(res.status, 200) + assert.strictEqual(res.ok, true) - return expect(res.text()).to.eventually.be.rejectedWith(Error) + return assert.rejects(res.text(), new TypeError('terminated')) }) }) it('should handle DNS-error response', () => { const url = 'http://domain.invalid' - return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) + return assert.rejects(fetch(url), new TypeError('fetch failed')) }) + // TODO: Should we pass through the error message? it('should reject invalid json response', () => { const url = `${base}error/json` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('application/json') - return expect(res.json()).to.eventually.be.rejectedWith(Error) + assert.strictEqual(res.headers.get('content-type'), 'application/json') + return assert.rejects(res.json(), SyntaxError) }) }) it('should handle response with no status text', () => { const url = `${base}no-status-text` return fetch(url).then(res => { - expect(res.statusText).to.equal('') + assert.strictEqual(res.statusText, '') }) }) it('should handle no content response', () => { const url = `${base}no-content` return fetch(url).then(res => { - expect(res.status).to.equal(204) - expect(res.statusText).to.equal('No Content') - expect(res.ok).to.be.true + assert.strictEqual(res.status, 204) + assert.strictEqual(res.statusText, 'No Content') + assert.strictEqual(res.ok, true) return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.be.empty + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) + // TODO: Should we pass through the error message? it('should reject when trying to parse no content response as json', () => { const url = `${base}no-content` return fetch(url).then(res => { - expect(res.status).to.equal(204) - expect(res.statusText).to.equal('No Content') - expect(res.ok).to.be.true - return expect(res.json()).to.eventually.be.rejectedWith(Error) + assert.strictEqual(res.status, 204) + assert.strictEqual(res.statusText, 'No Content') + assert.strictEqual(res.ok, true) + return assert.rejects(res.json(), new SyntaxError('Unexpected end of JSON input')) }) }) it('should handle no content response with gzip encoding', () => { const url = `${base}no-content/gzip` return fetch(url).then(res => { - expect(res.status).to.equal(204) - expect(res.statusText).to.equal('No Content') - expect(res.headers.get('content-encoding')).to.equal('gzip') - expect(res.ok).to.be.true + assert.strictEqual(res.status, 204) + assert.strictEqual(res.statusText, 'No Content') + assert.strictEqual(res.headers.get('content-encoding'), 'gzip') + assert.strictEqual(res.ok, true) return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.be.empty + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) @@ -604,12 +587,12 @@ describe('node-fetch', () => { it('should handle not modified response', () => { const url = `${base}not-modified` return fetch(url).then(res => { - expect(res.status).to.equal(304) - expect(res.statusText).to.equal('Not Modified') - expect(res.ok).to.be.false + assert.strictEqual(res.status, 304) + assert.strictEqual(res.statusText, 'Not Modified') + assert.strictEqual(res.ok, false) return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.be.empty + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) @@ -617,13 +600,13 @@ describe('node-fetch', () => { it('should handle not modified response with gzip encoding', () => { const url = `${base}not-modified/gzip` return fetch(url).then(res => { - expect(res.status).to.equal(304) - expect(res.statusText).to.equal('Not Modified') - expect(res.headers.get('content-encoding')).to.equal('gzip') - expect(res.ok).to.be.false + assert.strictEqual(res.status, 304) + assert.strictEqual(res.statusText, 'Not Modified') + assert.strictEqual(res.headers.get('content-encoding'), 'gzip') + assert.strictEqual(res.ok, false) return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.be.empty + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) @@ -631,10 +614,10 @@ describe('node-fetch', () => { it('should decompress gzip response', () => { const url = `${base}gzip` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) }) }) @@ -642,19 +625,19 @@ describe('node-fetch', () => { it('should decompress slightly invalid gzip response', async () => { const url = `${base}gzip-truncated` const res = await fetch(url) - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') const result = await res.text() - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) it('should decompress deflate response', () => { const url = `${base}deflate` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) }) }) @@ -662,10 +645,10 @@ describe('node-fetch', () => { it('should decompress deflate raw response from old apache server', () => { const url = `${base}deflate-raw` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) }) }) @@ -677,10 +660,10 @@ describe('node-fetch', () => { const url = `${base}brotli` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) }) }) @@ -692,13 +675,13 @@ describe('node-fetch', () => { const url = `${base}no-content/brotli` return fetch(url).then(res => { - expect(res.status).to.equal(204) - expect(res.statusText).to.equal('No Content') - expect(res.headers.get('content-encoding')).to.equal('br') - expect(res.ok).to.be.true + assert.strictEqual(res.status, 204) + assert.strictEqual(res.statusText, 'No Content') + assert.strictEqual(res.headers.get('content-encoding'), 'br') + assert.strictEqual(res.ok, true) return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.be.empty + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, '') }) }) }) @@ -706,10 +689,10 @@ describe('node-fetch', () => { it('should skip decompression if unsupported', () => { const url = `${base}sdch` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('fake sdch string') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'fake sdch string') }) }) }) @@ -717,10 +700,10 @@ describe('node-fetch', () => { it('should skip decompression if unsupported codings', () => { const url = `${base}multiunsupported` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('multiunsupported') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'multiunsupported') }) }) }) @@ -728,10 +711,10 @@ describe('node-fetch', () => { it('should decompress multiple coding', () => { const url = `${base}multisupported` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'hello world') }) }) }) @@ -739,8 +722,8 @@ describe('node-fetch', () => { it('should reject if response compression is invalid', () => { const url = `${base}invalid-content-encoding` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') - return expect(res.text()).to.eventually.be.rejected + assert.strictEqual(res.headers.get('content-type'), 'text/plain') + return assert.rejects(res.text(), new TypeError('terminated')) }) }) @@ -748,22 +731,22 @@ describe('node-fetch', () => { const url = `${base}invalid-content-encoding` fetch(url) .then(res => { - expect(res.status).to.equal(200) + assert.strictEqual(res.status, 200) }) - .catch(() => {}) - .then(() => { + .catch(() => { }) + .then(new Promise((resolve) => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { - done() + resolve() }, 20) - }) + })) }) it('should collect handled errors on the body stream to reject if the body is used later', () => { const url = `${base}invalid-content-encoding` return fetch(url).then(delay(20)).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') - return expect(res.text()).to.eventually.be.rejected + assert.strictEqual(res.headers.get('content-type'), 'text/plain') + return assert.rejects(res.text(), new TypeError('terminated')) }) }) @@ -776,7 +759,7 @@ describe('node-fetch', () => { } } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.headers['accept-encoding']).to.equal('gzip') + assert.strictEqual(res.headers['accept-encoding'], 'gzip') }) }) @@ -804,11 +787,15 @@ describe('node-fetch', () => { controller.abort() - return Promise.all(fetches.map(fetched => expect(fetched) - .to.eventually.be.rejected - .and.be.an.instanceOf(Error) - .and.have.property('name', 'AbortError') - )) + return Promise.all(fetches.map(async fetched => { + try { + await fetched + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } + })) }) it('should support multiple request cancellation with signal', () => { @@ -829,100 +816,118 @@ describe('node-fetch', () => { controller.abort() - return Promise.all(fetches.map(fetched => expect(fetched) - .to.eventually.be.rejected - .and.be.an.instanceOf(Error) - .and.have.property('name', 'AbortError') - )) + return Promise.all(fetches.map(async fetched => { + try { + await fetched + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } + })) }) - it('should reject immediately if signal has already been aborted', () => { + it('should reject immediately if signal has already been aborted', async () => { const url = `${base}timeout` const options = { signal: controller.signal } controller.abort() const fetched = fetch(url, options) - return expect(fetched).to.eventually.be.rejected - .and.be.an.instanceOf(Error) - .and.have.property('name', 'AbortError') + + try { + await fetched + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } }) - it('should allow redirects to be aborted', () => { + it('should allow redirects to be aborted', async () => { const request = new Request(`${base}redirect/slow`, { signal: controller.signal }) setTimeout(() => { controller.abort() }, 20) - return expect(fetch(request)).to.be.eventually.rejected - .and.be.an.instanceOf(Error) - .and.have.property('name', 'AbortError') + + try { + await fetch(request) + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } }) - it('should allow redirected response body to be aborted', () => { + it('should allow redirected response body to be aborted', async () => { const request = new Request(`${base}redirect/slow-stream`, { signal: controller.signal }) - return expect(fetch(request).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + const fetched = fetch(request).then(res => { + assert.strictEqual(res.headers.get('content-type'), 'text/plain') const result = res.text() controller.abort() return result - })).to.be.eventually.rejected - .and.be.an.instanceOf(Error) - .and.have.property('name', 'AbortError') + }) + + try { + await fetched + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } }) - it('should reject response body with AbortError when aborted before stream has been read completely', () => { - return expect(fetch( + it('should reject response body with AbortError when aborted before stream has been read completely', async () => { + const response = await fetch( `${base}slow`, { signal: controller.signal } - )) - .to.eventually.be.fulfilled - .then(res => { - const promise = res.text() - controller.abort() - return expect(promise) - .to.eventually.be.rejected - .and.be.an.instanceof(Error) - .and.have.property('name', 'AbortError') - }) + ) + + const promise = response.text() + controller.abort() + + try { + await promise + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } }) - it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => { - return expect(fetch( + it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', async () => { + const response = await fetch( `${base}slow`, { signal: controller.signal } - )) - .to.eventually.be.fulfilled - .then(res => { - controller.abort() - return expect(res.text()) - .to.eventually.be.rejected - .and.be.an.instanceof(Error) - .and.have.property('name', 'AbortError') - }) + ) + + controller.abort() + const promise = response.text() + try { + await promise + assert.fail('should have thrown') + } catch (error) { + assert.ok(error instanceof Error) + assert.strictEqual(error.name, 'AbortError') + } }) }) it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => { return Promise.all([ - expect(fetch(`${base}inspect`, { signal: {} })) - .to.be.eventually.rejected - .and.be.an.instanceof(TypeError), - expect(fetch(`${base}inspect`, { signal: '' })) - .to.be.eventually.rejected - .and.be.an.instanceof(TypeError), - expect(fetch(`${base}inspect`, { signal: Object.create(null) })) - .to.be.eventually.rejected - .and.be.an.instanceof(TypeError) + assert.rejects(fetch(`${base}inspect`, { signal: {} }), new TypeError("Failed to construct 'Request': member signal is not of type AbortSignal.")), + assert.rejects(fetch(`${base}inspect`, { signal: '' }), new TypeError("Failed to construct 'Request': member signal is not of type AbortSignal.")), + assert.rejects(fetch(`${base}inspect`, { signal: Object.create(null) }), new TypeError("Failed to construct 'Request': member signal is not of type AbortSignal.")) ]) }) it('should gracefully handle a null signal', () => { return fetch(`${base}hello`, { signal: null }).then(res => { - return expect(res.ok).to.be.true + return assert.strictEqual(res.ok, true) }) }) @@ -934,14 +939,14 @@ describe('node-fetch', () => { } } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.headers['user-agent']).to.equal('faked') + assert.strictEqual(res.headers['user-agent'], 'faked') }) }) it('should set default Accept header', () => { const url = `${base}inspect` fetch(url).then(res => res.json()).then(res => { - expect(res.headers.accept).to.equal('*/*') + assert.strictEqual(res.headers.accept, '*/*') }) }) @@ -953,7 +958,7 @@ describe('node-fetch', () => { } } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.headers.accept).to.equal('application/json') + assert.strictEqual(res.headers.accept, 'application/json') }) }) @@ -965,10 +970,10 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('0') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '0') }) }) @@ -981,11 +986,11 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') - expect(res.headers['content-length']).to.equal('3') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8') + assert.strictEqual(res.headers['content-length'], '3') }) }) @@ -998,11 +1003,11 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('3') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '3') }) }) @@ -1014,11 +1019,11 @@ describe('node-fetch', () => { body: encoder.encode('Hello, world!\n').buffer } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('Hello, world!\n') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('14') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'Hello, world!\n') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '14') }) }) @@ -1029,11 +1034,11 @@ describe('node-fetch', () => { body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('Hello, world!\n') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('14') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'Hello, world!\n') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '14') }) }) @@ -1045,11 +1050,11 @@ describe('node-fetch', () => { body: encoder.encode('Hello, world!\n') } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('Hello, world!\n') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('14') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'Hello, world!\n') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '14') }) }) @@ -1061,11 +1066,11 @@ describe('node-fetch', () => { body: new BigUint64Array(encoder.encode('0123456789abcdef').buffer) } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('0123456789abcdef') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('16') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, '0123456789abcdef') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '16') }) }) @@ -1077,11 +1082,11 @@ describe('node-fetch', () => { body: new DataView(encoder.encode('Hello, world!\n').buffer) } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('Hello, world!\n') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('14') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'Hello, world!\n') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '14') }) }) @@ -1092,11 +1097,11 @@ describe('node-fetch', () => { body: new VMUint8Array(Buffer.from('Hello, world!\n')) } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('Hello, world!\n') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('14') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'Hello, world!\n') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '14') }) }) @@ -1108,11 +1113,11 @@ describe('node-fetch', () => { body: encoder.encode('Hello, world!\n').subarray(7, 13) } return fetch(url, options).then(res => res.json()).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('world!') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('6') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'world!') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '6') }) }) @@ -1125,11 +1130,11 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.be.undefined - // expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.equal('3') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + // assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], '3') }) }) @@ -1144,11 +1149,11 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8') - expect(res.headers['content-length']).to.equal('3') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-type'], 'text/plain;charset=utf-8') + assert.strictEqual(res.headers['content-length'], '3') }) }) @@ -1162,11 +1167,11 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.equal('chunked') - expect(res.headers['content-type']).to.be.undefined - expect(res.headers['content-length']).to.be.undefined + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], 'chunked') + assert.strictEqual(res.headers['content-type'], undefined) + assert.strictEqual(res.headers['content-length'], undefined) }) }) @@ -1180,10 +1185,10 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.body).to.equal('[object Object]') - expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') - expect(res.headers['content-length']).to.equal('15') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.body, '[object Object]') + assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8') + assert.strictEqual(res.headers['content-length'], '15') }) }) @@ -1199,9 +1204,9 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary=') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'POST') + assert.ok(res.headers['content-type'].startsWith('multipart/form-data; boundary=')) + assert.strictEqual(res.body, 'a=1') }) }) @@ -1209,20 +1214,20 @@ describe('node-fetch', () => { const parameters = new URLSearchParams() const res = new Response(parameters) res.headers.get('Content-Type') - expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + assert.strictEqual(res.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8') }) it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { const parameters = new URLSearchParams() const request = new Request(base, { method: 'POST', body: parameters }) - expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + assert.strictEqual(request.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8') }) it('Reading a body with URLSearchParams should echo back the result', () => { const parameters = new URLSearchParams() parameters.append('a', '1') return new Response(parameters).text().then(text => { - expect(text).to.equal('a=1') + assert.strictEqual(text, 'a=1') }) }) @@ -1232,7 +1237,7 @@ describe('node-fetch', () => { const request = new Request(`${base}inspect`, { method: 'POST', body: parameters }) parameters.append('a', '1') return request.text().then(text => { - expect(text).to.equal('') + assert.strictEqual(text, '') }) }) @@ -1248,15 +1253,15 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') - expect(res.headers['content-length']).to.equal('3') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8') + assert.strictEqual(res.headers['content-length'], '3') + assert.strictEqual(res.body, 'a=1') }) }) it('should still recognize URLSearchParams when extended', () => { - class CustomSearchParameters extends URLSearchParams {} + class CustomSearchParameters extends URLSearchParams { } const parameters = new CustomSearchParameters() parameters.append('a', '1') @@ -1268,10 +1273,10 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('POST') - expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') - expect(res.headers['content-length']).to.equal('3') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'POST') + assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8') + assert.strictEqual(res.headers['content-length'], '3') + assert.strictEqual(res.body, 'a=1') }) }) @@ -1284,8 +1289,8 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('PUT') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'PUT') + assert.strictEqual(res.body, 'a=1') }) }) @@ -1297,7 +1302,7 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('DELETE') + assert.strictEqual(res.method, 'DELETE') }) }) @@ -1310,10 +1315,10 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('DELETE') - expect(res.body).to.equal('a=1') - expect(res.headers['transfer-encoding']).to.be.undefined - expect(res.headers['content-length']).to.equal('3') + assert.strictEqual(res.method, 'DELETE') + assert.strictEqual(res.body, 'a=1') + assert.strictEqual(res.headers['transfer-encoding'], undefined) + assert.strictEqual(res.headers['content-length'], '3') }) }) @@ -1326,8 +1331,8 @@ describe('node-fetch', () => { return fetch(url, options).then(res => { return res.json() }).then(res => { - expect(res.method).to.equal('PATCH') - expect(res.body).to.equal('a=1') + assert.strictEqual(res.method, 'PATCH') + assert.strictEqual(res.body, 'a=1') }) }) @@ -1337,13 +1342,13 @@ describe('node-fetch', () => { method: 'HEAD' } return fetch(url, options).then(res => { - expect(res.status).to.equal(200) - expect(res.statusText).to.equal('OK') - expect(res.headers.get('content-type')).to.equal('text/plain') - // expect(res.body).to.be.an.instanceof(stream.Transform) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.statusText, 'OK') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') + // assert.ok(res.body instanceof stream.Transform) return res.text() }).then(text => { - expect(text).to.equal('') + assert.strictEqual(text, '') }) }) @@ -1353,11 +1358,11 @@ describe('node-fetch', () => { method: 'HEAD' } return fetch(url, options).then(res => { - expect(res.status).to.equal(404) - expect(res.headers.get('content-encoding')).to.equal('gzip') + assert.strictEqual(res.status, 404) + assert.strictEqual(res.headers.get('content-encoding'), 'gzip') return res.text() }).then(text => { - expect(text).to.equal('') + assert.strictEqual(text, '') }) }) @@ -1367,20 +1372,20 @@ describe('node-fetch', () => { method: 'OPTIONS' } return fetch(url, options).then(res => { - expect(res.status).to.equal(200) - expect(res.statusText).to.equal('OK') - expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS') - // expect(res.body).to.be.an.instanceof(stream.Transform) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.statusText, 'OK') + assert.strictEqual(res.headers.get('allow'), 'GET, HEAD, OPTIONS') + // assert.ok(res.body instanceof stream.Transform) }) }) it('should reject decoding body twice', () => { const url = `${base}plain` return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') + assert.strictEqual(res.headers.get('content-type'), 'text/plain') return res.text().then(() => { - expect(res.bodyUsed).to.be.true - return expect(res.text()).to.eventually.be.rejectedWith(Error) + assert.strictEqual(res.bodyUsed, true) + return assert.rejects(res.text(), new TypeError('Body is unusable')) }) }) }) @@ -1390,8 +1395,8 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone() return Promise.all([res.json(), r1.text()]).then(results => { - expect(results[0]).to.deep.equal({ name: 'value' }) - expect(results[1]).to.equal('{"name":"value"}') + assert.deepStrictEqual(results[0], { name: 'value' }) + assert.strictEqual(results[1], '{"name":"value"}') }) }) }) @@ -1401,9 +1406,9 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone() return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }) + assert.deepStrictEqual(result, { name: 'value' }) return r1.text().then(result => { - expect(result).to.equal('{"name":"value"}') + assert.strictEqual(result, '{"name":"value"}') }) }) }) @@ -1414,9 +1419,9 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone() return r1.text().then(result => { - expect(result).to.equal('{"name":"value"}') + assert.strictEqual(result, '{"name":"value"}') return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }) + assert.deepStrictEqual(result, { name: 'value' }) }) }) }) @@ -1426,15 +1431,15 @@ describe('node-fetch', () => { const url = `${base}hello` return fetch(url).then(res => res.text().then(() => { - expect(() => { + assert.throws(() => { res.clone() - }).to.throw(Error) + }, new TypeError('Response.clone: Body has already been consumed.')) }) ) }) - xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { - this.timeout(300) + // TODO: fix test. + it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', { timeout: 300 }, function () { const url = local.mockState(res => { // Observed behavior of TCP packets splitting: // - response body size <= 65438 → single packet sent @@ -1450,8 +1455,8 @@ describe('node-fetch', () => { ).to.timeout }) - xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { - this.timeout(300) + // TODO: fix test. + it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', { timeout: 300 }, function () { const url = local.mockState(res => { const firstPacketMaxSize = 65438 const secondPacketSize = 10 @@ -1462,13 +1467,8 @@ describe('node-fetch', () => { ).to.timeout }) - xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { - // TODO: fix test. - if (!isNodeLowerThan('v16.0.0')) { - this.skip() - } - - this.timeout(300) + // TODO: fix test. + it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', { timeout: 300 }, async function () { const url = local.mockState(res => { const firstPacketMaxSize = 65438 const secondPacketSize = 16 * 1024 // = defaultHighWaterMark @@ -1479,13 +1479,8 @@ describe('node-fetch', () => { ).not.to.timeout }) - xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { - // TODO: fix test. - if (!isNodeLowerThan('v16.0.0')) { - this.skip() - } - - this.timeout(300) + // TODO: fix test. + it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', { timeout: 300 }, function () { const url = local.mockState(res => { const firstPacketMaxSize = 65438 const secondPacketSize = 10 @@ -1496,13 +1491,8 @@ describe('node-fetch', () => { ).not.to.timeout }) - xit('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { - // TODO: fix test. - if (!isNodeLowerThan('v16.0.0')) { - this.skip() - } - - this.timeout(300) + // TODO: fix test. + it.skip('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', { timeout: 300 }, function () { const url = local.mockState(res => { res.end(crypto.randomBytes((2 * 512 * 1024) - 1)) }) @@ -1511,13 +1501,13 @@ describe('node-fetch', () => { ).not.to.timeout }) - xit('should allow get all responses of a header', () => { - // TODO: fix test. + // TODO: fix test. + it.skip('should allow get all responses of a header', () => { const url = `${base}cookie` return fetch(url).then(res => { const expected = 'a=1, b=1' - expect(res.headers.get('set-cookie')).to.equal(expected) - expect(res.headers.get('Set-Cookie')).to.equal(expected) + assert.strictEqual(res.headers.get('set-cookie'), expected) + assert.strictEqual(res.headers.get('Set-Cookie'), expected) }) }) @@ -1525,9 +1515,9 @@ describe('node-fetch', () => { const url = `${base}hello` const request = new Request(url) return fetch(request).then(res => { - expect(res.url).to.equal(url) - expect(res.ok).to.be.true - expect(res.status).to.equal(200) + assert.strictEqual(res.url, url) + assert.strictEqual(res.ok, true) + assert.strictEqual(res.status, 200) }) }) @@ -1536,9 +1526,9 @@ describe('node-fetch', () => { const urlObject = new URL(url) const request = new Request(urlObject) return fetch(request).then(res => { - expect(res.url).to.equal(url) - expect(res.ok).to.be.true - expect(res.status).to.equal(200) + assert.strictEqual(res.url, url) + assert.strictEqual(res.ok, true) + assert.strictEqual(res.status, 200) }) }) @@ -1547,9 +1537,9 @@ describe('node-fetch', () => { const urlObject = new URL(url) const request = new Request(urlObject) return fetch(request).then(res => { - expect(res.url).to.equal(url) - expect(res.ok).to.be.true - expect(res.status).to.equal(200) + assert.strictEqual(res.url, url) + assert.strictEqual(res.ok, true) + assert.strictEqual(res.status, 200) }) }) @@ -1558,9 +1548,9 @@ describe('node-fetch', () => { const urlObject = new URL(url) const request = new Request(urlObject) return fetch(request).then(res => { - expect(res.url).to.equal(url) - expect(res.ok).to.be.true - expect(res.status).to.equal(200) + assert.strictEqual(res.url, url) + assert.strictEqual(res.ok, true) + assert.strictEqual(res.status, 200) }) }) @@ -1569,7 +1559,7 @@ describe('node-fetch', () => { .blob() .then(blob => blob.text()) .then(body => { - expect(body).to.equal('hello') + assert.strictEqual(body, 'hello') }) }) @@ -1579,7 +1569,7 @@ describe('node-fetch', () => { .then(blob => blob.arrayBuffer()) .then(ab => { const string = String.fromCharCode.apply(null, new Uint8Array(ab)) - expect(string).to.equal('hello') + assert.strictEqual(string, 'hello') }) }) @@ -1598,9 +1588,9 @@ describe('node-fetch', () => { body: blob }) }).then(res => res.json()).then(({ body, headers }) => { - expect(body).to.equal('world') - expect(headers['content-type']).to.equal(type) - expect(headers['content-length']).to.equal(String(length)) + assert.strictEqual(body, 'world') + assert.strictEqual(headers['content-type'], type) + assert.strictEqual(headers['content-length'], String(length)) }) }) @@ -1620,41 +1610,39 @@ describe('node-fetch', () => { }).then(res => { return res.json() }).then(body => { - expect(body.method).to.equal('GET') - expect(body.headers.a).to.equal('2') + assert.strictEqual(body.method, 'GET') + assert.strictEqual(body.headers.a, '2') }) }) - it('should support http request', function () { - this.timeout(5000) + it('should support http request', { timeout: 5000 }, function () { const url = 'https://github.com/' const options = { method: 'HEAD' } return fetch(url, options).then(res => { - expect(res.status).to.equal(200) - expect(res.ok).to.be.true + assert.strictEqual(res.status, 200) + assert.strictEqual(res.ok, true) }) }) it('should encode URLs as UTF-8', async () => { const url = `${base}möbius` const res = await fetch(url) - expect(res.url).to.equal(`${base}m%C3%B6bius`) + assert.strictEqual(res.url, `${base}m%C3%B6bius`) }) - it('should allow manual redirect handling', function () { - this.timeout(5000) + it('should allow manual redirect handling', { timeout: 5000 }, function () { const url = `${base}redirect/302` const options = { redirect: 'manual' } return fetch(url, options).then(res => { - expect(res.status).to.equal(302) - expect(res.url).to.equal(url) - expect(res.type).to.equal('basic') - expect(res.headers.get('Location')).to.equal('/inspect') - expect(res.ok).to.be.false + assert.strictEqual(res.status, 302) + assert.strictEqual(res.url, url) + assert.strictEqual(res.type, 'basic') + assert.strictEqual(res.headers.get('Location'), '/inspect') + assert.strictEqual(res.ok, false) }) }) }) diff --git a/test/node-fetch/mock.js b/test/node-fetch/mock.js index a53f464a1a9..f9835484328 100644 --- a/test/node-fetch/mock.js +++ b/test/node-fetch/mock.js @@ -1,7 +1,8 @@ -/* eslint no-unused-expressions: "off" */ +'use strict' // Test tools -const chai = require('chai') +const assert = require('node:assert') +const { describe, it } = require('node:test') const { fetch, @@ -10,8 +11,6 @@ const { Headers } = require('../../index.js') -const { expect } = chai - describe('node-fetch with MockAgent', () => { it('should match the url', async () => { const mockAgent = new MockAgent() @@ -30,8 +29,8 @@ describe('node-fetch with MockAgent', () => { method: 'GET' }) - expect(res.status).to.equal(200) - expect(await res.json()).to.deep.equal({ success: true }) + assert.strictEqual(res.status, 200) + assert.deepStrictEqual(await res.json(), { success: true }) }) it('should match the body', async () => { @@ -55,8 +54,8 @@ describe('node-fetch with MockAgent', () => { body: 'request body' }) - expect(res.status).to.equal(200) - expect(await res.json()).to.deep.equal({ success: true }) + assert.strictEqual(res.status, 200) + assert.deepStrictEqual(await res.json(), { success: true }) }) it('should match the headers', async () => { @@ -79,8 +78,9 @@ describe('node-fetch with MockAgent', () => { method: 'GET', headers: new Headers({ 'User-Agent': 'undici' }) }) - expect(res.status).to.equal(200) - expect(await res.json()).to.deep.equal({ success: true }) + + assert.strictEqual(res.status, 200) + assert.deepStrictEqual(await res.json(), { success: true }) }) it('should match the headers with a matching function', async () => { @@ -93,8 +93,8 @@ describe('node-fetch with MockAgent', () => { path: '/test', method: 'GET', headers (headers) { - expect(headers).to.be.an('object') - expect(headers).to.have.property('user-agent', 'undici') + assert.strictEqual(typeof headers, 'object') + assert.strictEqual(headers['user-agent'], 'undici') return true } }) @@ -106,7 +106,7 @@ describe('node-fetch with MockAgent', () => { headers: new Headers({ 'User-Agent': 'undici' }) }) - expect(res.status).to.equal(200) - expect(await res.json()).to.deep.equal({ success: true }) + assert.strictEqual(res.status, 200) + assert.deepStrictEqual(await res.json(), { success: true }) }) }) diff --git a/test/node-fetch/request.js b/test/node-fetch/request.js index 405bc2c467e..505dcc3eebb 100644 --- a/test/node-fetch/request.js +++ b/test/node-fetch/request.js @@ -1,14 +1,14 @@ +'use strict' + +const assert = require('node:assert') +const { describe, it, before, after } = require('node:test') const stream = require('node:stream') const http = require('node:http') - -const chai = require('chai') const { Blob } = require('node:buffer') -const Request = require('../../lib/fetch/request.js').Request +const { Request } = require('../../index.js') const TestServer = require('./utils/server.js') -const { expect } = chai - describe('Request', () => { const local = new TestServer() let base @@ -43,47 +43,47 @@ describe('Request', () => { 'clone', 'signal' ]) { - expect(enumerableProperties).to.contain(toCheck) + assert.ok(enumerableProperties.includes(toCheck)) } - // for (const toCheck of [ - // 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' - // ]) { - // expect(() => { - // request[toCheck] = 'abc' - // }).to.throw() - // } + for (const toCheck of [ + 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' + ]) { + assert.throws(() => { + request[toCheck] = 'abc' + }, new TypeError(`Cannot set property ${toCheck} of # which has only a getter`)) + } }) - // it('should support wrapping Request instance', () => { - // const url = `${base}hello` + it.skip('should support wrapping Request instance', () => { + const url = `${base}hello` - // const form = new FormData() - // form.append('a', '1') - // const { signal } = new AbortController() + const form = new FormData() + form.append('a', '1') + const { signal } = new AbortController() - // const r1 = new Request(url, { - // method: 'POST', - // follow: 1, - // body: form, - // signal - // }) - // const r2 = new Request(r1, { - // follow: 2 - // }) + const r1 = new Request(url, { + method: 'POST', + follow: 1, + body: form, + signal + }) + const r2 = new Request(r1, { + follow: 2 + }) - // expect(r2.url).to.equal(url) - // expect(r2.method).to.equal('POST') - // expect(r2.signal).to.equal(signal) - // // Note that we didn't clone the body - // expect(r2.body).to.equal(form) - // expect(r1.follow).to.equal(1) - // expect(r2.follow).to.equal(2) - // expect(r1.counter).to.equal(0) - // expect(r2.counter).to.equal(0) - // }) + assert.strictEqual(r2.url, url) + assert.strictEqual(r2.method, 'POST') + assert.strictEqual(r2.signal[Symbol.toStringTag], 'AbortSignal') + // Note that we didn't clone the body + assert.strictEqual(r2.body, form) + assert.strictEqual(r1.follow, 1) + assert.strictEqual(r2.follow, 2) + assert.strictEqual(r1.counter, 0) + assert.strictEqual(r2.counter, 0) + }) - xit('should override signal on derived Request instances', () => { + it.skip('should override signal on derived Request instances', () => { const parentAbortController = new AbortController() const derivedAbortController = new AbortController() const parentRequest = new Request(`${base}hello`, { @@ -92,11 +92,11 @@ describe('Request', () => { const derivedRequest = new Request(parentRequest, { signal: derivedAbortController.signal }) - expect(parentRequest.signal).to.equal(parentAbortController.signal) - expect(derivedRequest.signal).to.equal(derivedAbortController.signal) + assert.strictEqual(parentRequest.signal, parentAbortController.signal) + assert.strictEqual(derivedRequest.signal, derivedAbortController.signal) }) - xit('should allow removing signal on derived Request instances', () => { + it.skip('should allow removing signal on derived Request instances', () => { const parentAbortController = new AbortController() const parentRequest = new Request(`${base}hello`, { signal: parentAbortController.signal @@ -104,29 +104,25 @@ describe('Request', () => { const derivedRequest = new Request(parentRequest, { signal: null }) - expect(parentRequest.signal).to.equal(parentAbortController.signal) - expect(derivedRequest.signal).to.equal(null) + assert.strictEqual(parentRequest.signal, parentAbortController.signal) + assert.strictEqual(derivedRequest.signal, null) }) it('should throw error with GET/HEAD requests with body', () => { - expect(() => new Request(base, { body: '' })) - .to.throw(TypeError) - expect(() => new Request(base, { body: 'a' })) - .to.throw(TypeError) - expect(() => new Request(base, { body: '', method: 'HEAD' })) - .to.throw(TypeError) - expect(() => new Request(base, { body: 'a', method: 'HEAD' })) - .to.throw(TypeError) - expect(() => new Request(base, { body: 'a', method: 'get' })) - .to.throw(TypeError) - expect(() => new Request(base, { body: 'a', method: 'head' })) - .to.throw(TypeError) + assert.throws(() => new Request(base, { body: '' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: 'a' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: '', method: 'HEAD' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: 'a', method: 'HEAD' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: '', method: 'get' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: 'a', method: 'get' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: '', method: 'head' }), new TypeError('Request with GET/HEAD method cannot have body.')) + assert.throws(() => new Request(base, { body: 'a', method: 'head' }), new TypeError('Request with GET/HEAD method cannot have body.')) }) it('should default to null as body', () => { const request = new Request(base) - expect(request.body).to.equal(null) - return request.text().then(result => expect(result).to.equal('')) + assert.strictEqual(request.body, null) + return request.text().then(result => assert.strictEqual(result, '')) }) it('should support parsing headers', () => { @@ -136,8 +132,8 @@ describe('Request', () => { a: '1' } }) - expect(request.url).to.equal(url) - expect(request.headers.get('a')).to.equal('1') + assert.strictEqual(request.url, url) + assert.strictEqual(request.headers.get('a'), '1') }) it('should support arrayBuffer() method', () => { @@ -146,11 +142,11 @@ describe('Request', () => { method: 'POST', body: 'a=1' }) - expect(request.url).to.equal(url) + assert.strictEqual(request.url, url) return request.arrayBuffer().then(result => { - expect(result).to.be.an.instanceOf(ArrayBuffer) + assert.ok(result instanceof ArrayBuffer) const string = String.fromCharCode.apply(null, new Uint8Array(result)) - expect(string).to.equal('a=1') + assert.strictEqual(string, 'a=1') }) }) @@ -160,9 +156,9 @@ describe('Request', () => { method: 'POST', body: 'a=1' }) - expect(request.url).to.equal(url) + assert.strictEqual(request.url, url) return request.text().then(result => { - expect(result).to.equal('a=1') + assert.strictEqual(result, 'a=1') }) }) @@ -172,9 +168,9 @@ describe('Request', () => { method: 'POST', body: '{"a":1}' }) - expect(request.url).to.equal(url) + assert.strictEqual(request.url, url) return request.json().then(result => { - expect(result.a).to.equal(1) + assert.strictEqual(result.a, 1) }) }) @@ -184,11 +180,11 @@ describe('Request', () => { method: 'POST', body: Buffer.from('a=1') }) - expect(request.url).to.equal(url) + assert.strictEqual(request.url, url) return request.blob().then(result => { - expect(result).to.be.an.instanceOf(Blob) - expect(result.size).to.equal(3) - expect(result.type).to.equal('') + assert.ok(result instanceof Blob) + assert.strictEqual(result.size, 3) + assert.strictEqual(result.type, '') }) }) @@ -211,16 +207,16 @@ describe('Request', () => { duplex: 'half' }) const cl = request.clone() - expect(cl.url).to.equal(url) - expect(cl.method).to.equal('POST') - expect(cl.redirect).to.equal('manual') - expect(cl.headers.get('b')).to.equal('2') - expect(cl.method).to.equal('POST') + assert.strictEqual(cl.url, url) + assert.strictEqual(cl.method, 'POST') + assert.strictEqual(cl.redirect, 'manual') + assert.strictEqual(cl.headers.get('b'), '2') + assert.strictEqual(cl.method, 'POST') // Clone body shouldn't be the same body - expect(cl.body).to.not.equal(body) + assert.notDeepEqual(cl.body, body) return Promise.all([cl.text(), request.text()]).then(results => { - expect(results[0]).to.equal('a=1') - expect(results[1]).to.equal('a=1') + assert.strictEqual(results[0], 'a=1') + assert.strictEqual(results[1], 'a=1') }) }) @@ -233,7 +229,7 @@ describe('Request', () => { }) new Uint8Array(body)[0] = 0 return request.text().then(result => { - expect(result).to.equal('a=12345678901234') + assert.strictEqual(result, 'a=12345678901234') }) }) @@ -247,7 +243,7 @@ describe('Request', () => { }) body[0] = 0 return request.text().then(result => { - expect(result).to.equal('123456789') + assert.strictEqual(result, '123456789') }) }) @@ -261,7 +257,7 @@ describe('Request', () => { }) body[0] = 0n return request.text().then(result => { - expect(result).to.equal('78901234') + assert.strictEqual(result, '78901234') }) }) @@ -275,7 +271,7 @@ describe('Request', () => { }) body[0] = 0 return request.text().then(result => { - expect(result).to.equal('123456789') + assert.strictEqual(result, '123456789') }) }) }) diff --git a/test/node-fetch/response.js b/test/node-fetch/response.js index b928651edfa..e28dcb37119 100644 --- a/test/node-fetch/response.js +++ b/test/node-fetch/response.js @@ -1,14 +1,13 @@ -/* eslint no-unused-expressions: "off" */ +'use strict' -const chai = require('chai') +const assert = require('node:assert') +const { describe, it, before, after } = require('node:test') const stream = require('node:stream') -const { Response } = require('../../lib/fetch/response.js') +const { Response } = require('../../index.js') const TestServer = require('./utils/server.js') const { Blob } = require('node:buffer') const { kState } = require('../../lib/fetch/symbols.js') -const { expect } = chai - describe('Response', () => { const local = new TestServer() let base @@ -45,32 +44,30 @@ describe('Response', () => { 'headers', 'clone' ]) { - expect(enumerableProperties).to.contain(toCheck) + assert.ok(enumerableProperties.includes(toCheck)) } - // TODO - // for (const toCheck of [ - // 'body', - // 'bodyUsed', - // 'type', - // 'url', - // 'status', - // 'ok', - // 'redirected', - // 'statusText', - // 'headers' - // ]) { - // expect(() => { - // res[toCheck] = 'abc' - // }).to.throw() - // } - }) - - it('should support empty options', () => { + for (const toCheck of [ + 'body', + 'bodyUsed', + 'type', + 'url', + 'status', + 'ok', + 'redirected', + 'statusText', + 'headers' + ]) { + assert.throws(() => { + res[toCheck] = 'abc' + }, new TypeError(`Cannot set property ${toCheck} of # which has only a getter`)) + } + }) + + it('should support empty options', async () => { const res = new Response(stream.Readable.from('a=1')) - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + assert.strictEqual(result, 'a=1') }) it('should support parsing headers', () => { @@ -79,36 +76,33 @@ describe('Response', () => { a: '1' } }) - expect(res.headers.get('a')).to.equal('1') + assert.strictEqual(res.headers.get('a'), '1') }) - it('should support text() method', () => { + it('should support text() method', async () => { const res = new Response('a=1') - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + assert.strictEqual(result, 'a=1') }) - it('should support json() method', () => { + it('should support json() method', async () => { const res = new Response('{"a":1}') - return res.json().then(result => { - expect(result.a).to.equal(1) - }) + const result = await res.json() + assert.deepStrictEqual(result, { a: 1 }) }) if (Blob) { - it('should support blob() method', () => { + it('should support blob() method', async () => { const res = new Response('a=1', { method: 'POST', headers: { 'Content-Type': 'text/plain' } }) - return res.blob().then(result => { - expect(result).to.be.an.instanceOf(Blob) - expect(result.size).to.equal(3) - expect(result.type).to.equal('text/plain') - }) + const result = await res.blob() + assert.ok(result instanceof Blob) + assert.strictEqual(result.size, 3) + assert.strictEqual(result.type, 'text/plain') }) } @@ -123,129 +117,129 @@ describe('Response', () => { }) res[kState].urlList = [new URL(base)] const cl = res.clone() - expect(cl.headers.get('a')).to.equal('1') - expect(cl.type).to.equal('default') - expect(cl.url).to.equal(base) - expect(cl.status).to.equal(346) - expect(cl.statusText).to.equal('production') - expect(cl.ok).to.be.false + assert.strictEqual(cl.headers.get('a'), '1') + assert.strictEqual(cl.type, 'default') + assert.strictEqual(cl.url, base) + assert.strictEqual(cl.status, 346) + assert.strictEqual(cl.statusText, 'production') + assert.strictEqual(cl.ok, false) // Clone body shouldn't be the same body - expect(cl.body).to.not.equal(body) + assert.notStrictEqual(cl.body, body) return Promise.all([cl.text(), res.text()]).then(results => { - expect(results[0]).to.equal('a=1') - expect(results[1]).to.equal('a=1') + assert.strictEqual(results[0], 'a=1') + assert.strictEqual(results[1], 'a=1') }) }) - it('should support stream as body', () => { + it('should support stream as body', async () => { const body = stream.Readable.from('a=1') const res = new Response(body) - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + + assert.strictEqual(result, 'a=1') }) - it('should support string as body', () => { + it('should support string as body', async () => { const res = new Response('a=1') - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + + assert.strictEqual(result, 'a=1') }) - it('should support buffer as body', () => { + it('should support buffer as body', async () => { const res = new Response(Buffer.from('a=1')) - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + + assert.strictEqual(result, 'a=1') }) - it('should support ArrayBuffer as body', () => { + it('should support ArrayBuffer as body', async () => { const encoder = new TextEncoder() const fullbuffer = encoder.encode('a=12345678901234').buffer const res = new Response(fullbuffer) new Uint8Array(fullbuffer)[0] = 0 - return res.text().then(result => { - expect(result).to.equal('a=12345678901234') - }) + + const result = await res.text() + assert.strictEqual(result, 'a=12345678901234') }) it('should support blob as body', async () => { const res = new Response(new Blob(['a=1'])) - return res.text().then(result => { - expect(result).to.equal('a=1') - }) + const result = await res.text() + + assert.strictEqual(result, 'a=1') }) - it('should support Uint8Array as body', () => { + it('should support Uint8Array as body', async () => { const encoder = new TextEncoder() const fullbuffer = encoder.encode('a=12345678901234').buffer const body = new Uint8Array(fullbuffer, 2, 9) const res = new Response(body) body[0] = 0 - return res.text().then(result => { - expect(result).to.equal('123456789') - }) + + const result = await res.text() + assert.strictEqual(result, '123456789') }) - it('should support BigUint64Array as body', () => { + it('should support BigUint64Array as body', async () => { const encoder = new TextEncoder() const fullbuffer = encoder.encode('a=12345678901234').buffer const body = new BigUint64Array(fullbuffer, 8, 1) const res = new Response(body) body[0] = 0n - return res.text().then(result => { - expect(result).to.equal('78901234') - }) + + const result = await res.text() + assert.strictEqual(result, '78901234') }) - it('should support DataView as body', () => { + it('should support DataView as body', async () => { const encoder = new TextEncoder() const fullbuffer = encoder.encode('a=12345678901234').buffer const body = new Uint8Array(fullbuffer, 2, 9) const res = new Response(body) body[0] = 0 - return res.text().then(result => { - expect(result).to.equal('123456789') - }) + + const result = await res.text() + assert.strictEqual(result, '123456789') }) it('should default to null as body', () => { const res = new Response() - expect(res.body).to.equal(null) + assert.strictEqual(res.body, null) - return res.text().then(result => expect(result).to.equal('')) + return res.text().then(result => assert.strictEqual(result, '')) }) it('should default to 200 as status code', () => { const res = new Response(null) - expect(res.status).to.equal(200) + assert.strictEqual(res.status, 200) }) it('should default to empty string as url', () => { const res = new Response() - expect(res.url).to.equal('') + assert.strictEqual(res.url, '') }) it('should support error() static method', () => { const res = Response.error() - expect(res).to.be.an.instanceof(Response) - expect(res.type).to.equal('error') - expect(res.status).to.equal(0) - expect(res.statusText).to.equal('') + assert.ok(res instanceof Response) + assert.strictEqual(res.status, 0) + assert.strictEqual(res.statusText, '') + assert.strictEqual(res.type, 'error') }) it('should support undefined status', () => { const res = new Response(null, { status: undefined }) - expect(res.status).to.equal(200) + assert.strictEqual(res.status, 200) }) it('should support undefined statusText', () => { const res = new Response(null, { statusText: undefined }) - expect(res.statusText).to.equal('') + assert.strictEqual(res.statusText, '') }) it('should not set bodyUsed to undefined', () => { const res = new Response() - expect(res.bodyUsed).to.be.false + assert.strictEqual(res.bodyUsed, false) }) }) diff --git a/test/node-fetch/utils/chai-timeout.js b/test/node-fetch/utils/chai-timeout.js deleted file mode 100644 index 6838a4cc322..00000000000 --- a/test/node-fetch/utils/chai-timeout.js +++ /dev/null @@ -1,15 +0,0 @@ -const pTimeout = require('p-timeout') - -module.exports = ({ Assertion }, utils) => { - utils.addProperty(Assertion.prototype, 'timeout', async function () { - let timeouted = false - await pTimeout(this._obj, 150, () => { - timeouted = true - }) - return this.assert( - timeouted, - 'expected promise to timeout but it was resolved', - 'expected promise not to timeout but it timed out' - ) - }) -} From e30f200b84ff7f862f61fe5c69185aaa816c5b25 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 9 Feb 2024 12:29:22 +0100 Subject: [PATCH 007/123] test: replace t.pass with t.ok (#2721) --- test/client-keep-alive.js | 20 ++++++++--------- test/client-pipeline.js | 12 +++++----- test/client-pipelining.js | 24 ++++++++++---------- test/client-post.js | 4 ++-- test/client-reconnect.js | 4 ++-- test/client-request.js | 30 ++++++++++++------------- test/client-stream.js | 4 ++-- test/client-timeout.js | 2 +- test/client-upgrade.js | 2 +- test/client-write-max-listeners.js | 2 +- test/client.js | 36 +++++++++++++++--------------- test/close-and-destroy.js | 2 +- test/connect-errconnect.js | 2 +- test/content-length.js | 12 +++++----- test/get-head-body.js | 6 ++--- test/http-100.js | 6 ++--- test/http2.js | 2 +- test/inflight-and-close.js | 6 ++--- test/issue-810.js | 8 +++---- test/max-headers.js | 2 +- test/node-test/client-dispatch.js | 8 +++---- test/parser-issues.js | 2 +- test/pipeline-pipelining.js | 4 ++-- test/pool.js | 20 ++++++++--------- test/proxy-agent.js | 32 +++++++++++++------------- test/redirect-request.js | 2 +- test/retry-handler.js | 36 +++++++++++++++--------------- test/socket-back-pressure.js | 2 +- test/socket-timeout.js | 2 +- test/tls-session-reuse.js | 4 ++-- test/tls.js | 16 ++++++------- 31 files changed, 157 insertions(+), 157 deletions(-) diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index 961a44f1a8b..15e432c7b2c 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -34,7 +34,7 @@ test('keep-alive header', (t) => { t.fail() }, 4e3) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -76,7 +76,7 @@ test('keep-alive header 0', (t) => { t.error(err) body.on('end', () => { client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) clock.tick(600) }).resume() @@ -110,7 +110,7 @@ test('keep-alive header 1', (t) => { t.fail() }, 0) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -144,7 +144,7 @@ test('keep-alive header no postfix', (t) => { t.fail() }, 4e3) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -180,7 +180,7 @@ test('keep-alive not timeout', (t) => { t.fail() }, 3e3) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -217,7 +217,7 @@ test('keep-alive threshold', (t) => { t.fail() }, 5e3) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -254,7 +254,7 @@ test('keep-alive max keepalive', (t) => { t.fail() }, 3e3) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -296,7 +296,7 @@ test('connection close', (t) => { }, 3e3) client.once('disconnect', () => { close = false - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -312,7 +312,7 @@ test('connection close', (t) => { t.fail() }, 3e3) client.once('disconnect', () => { - t.pass() + t.ok(true, 'pass') clearTimeout(timeout) }) }).resume() @@ -350,7 +350,7 @@ test('Disable keep alive', (t) => { }, (err, { body }) => { t.error(err) body.on('end', () => { - t.pass() + t.ok(true, 'pass') }).resume() }) }).resume() diff --git a/test/client-pipeline.js b/test/client-pipeline.js index 300e42c7eb2..e37657456ff 100644 --- a/test/client-pipeline.js +++ b/test/client-pipeline.js @@ -201,7 +201,7 @@ test('pipeline invalid handler return after destroy should not error', (t) => { t.equal(err.message, 'asd') }) .on('close', () => { - t.pass() + t.ok(true, 'pass') }) .end() }) @@ -315,7 +315,7 @@ test('pipeline backpressure', (t) => { duplex.resume() }) }).on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -412,7 +412,7 @@ test('pipeline destroy and throw handler', (t) => { t.type(err, errors.RequestAbortedError) }) .on('close', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -443,7 +443,7 @@ test('pipeline abort res', (t) => { }, 100) client.on('disconnect', () => { clearTimeout(timeout) - t.pass() + t.ok(true, 'pass') }) }) return body @@ -789,7 +789,7 @@ test('pipeline legacy stream', (t) => { }) .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) .end() }) @@ -896,7 +896,7 @@ test('pipeline body without destroy', (t) => { }) .end() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) .resume() }) diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 38a99f54c5b..25441407f43 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -355,7 +355,7 @@ function errordInflightPost (bodyType) { serverRes.end() }) .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -391,7 +391,7 @@ test('pipelining non-idempotent', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') ended = true }) }) @@ -449,7 +449,7 @@ function pipeliningNonIdempotentWithBody (bodyType) { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) @@ -504,7 +504,7 @@ function pipeliningHeadBusy (bodyType) { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) body.push(null) @@ -525,7 +525,7 @@ function pipeliningHeadBusy (bodyType) { .resume() .on('end', () => { ended = true - t.pass() + t.ok(true, 'pass') }) }) body.push(null) @@ -575,7 +575,7 @@ test('pipelining empty pipeline before reset', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) t.equal(client[kBusy], false) @@ -590,7 +590,7 @@ test('pipelining empty pipeline before reset', (t) => { .resume() .on('end', () => { ended = true - t.pass() + t.ok(true, 'pass') }) }) t.equal(client[kBusy], true) @@ -628,7 +628,7 @@ function pipeliningIdempotentBusy (bodyType) { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) body.push(null) @@ -649,7 +649,7 @@ function pipeliningIdempotentBusy (bodyType) { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) body.push(null) @@ -688,7 +688,7 @@ function pipeliningIdempotentBusy (bodyType) { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) body.push(null) @@ -734,7 +734,7 @@ test('pipelining blocked', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) client.request({ @@ -745,7 +745,7 @@ test('pipelining blocked', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/client-post.js b/test/client-post.js index fc696c951a5..82203ad36c7 100644 --- a/test/client-post.js +++ b/test/client-post.js @@ -32,7 +32,7 @@ test('request post blob', { skip: !Blob }, (t) => { }, (err, data) => { t.error(err) data.body.resume().on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -66,7 +66,7 @@ test('request post arrayBuffer', { skip: !Blob }, (t) => { }, (err, data) => { t.error(err) data.body.resume().on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/client-reconnect.js b/test/client-reconnect.js index 85f76ef5f62..d8ed2ce4ddd 100644 --- a/test/client-reconnect.js +++ b/test/client-reconnect.js @@ -38,13 +38,13 @@ test('multiple reconnect', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) client.on('disconnect', () => { if (++n === 1) { - t.pass() + t.ok(true, 'pass') } process.nextTick(() => { clock.tick(1000) diff --git a/test/client-request.js b/test/client-request.js index ebcc0c2e96f..aa9405b2fcc 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -36,7 +36,7 @@ test('request dump', (t) => { t.error(err) body.dump().then(() => { dumped = true - t.pass() + t.ok(true, 'pass') }) }) }) @@ -270,7 +270,7 @@ test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => { /* eslint-enable */ t.fail('no error') } catch (err) { - t.pass('error happened') + t.ok(true, 'error happened') } }) @@ -490,7 +490,7 @@ test('request post body no missing data', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -527,7 +527,7 @@ test('request post body no extra data handler', (t) => { maxRedirections: 0 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -552,7 +552,7 @@ test('request with onInfo callback', (t) => { }) t.equal(infos.length, 1) t.equal(infos[0].statusCode, 102) - t.pass() + t.ok(true, 'pass') }) }) @@ -585,7 +585,7 @@ test('request with onInfo callback but socket is destroyed before end of respons } t.equal(infos.length, 1) t.equal(infos[0].statusCode, 102) - t.pass() + t.ok(true, 'pass') }) }) @@ -622,7 +622,7 @@ test('request onInfo callback headers parsing', async (t) => { t.equal(infos.length, 1) t.equal(infos[0].statusCode, 103) t.same(infos[0].headers, { link: '; rel=preload; as=style' }) - t.pass() + t.ok(true, 'pass') }) test('request raw responseHeaders', async (t) => { @@ -659,7 +659,7 @@ test('request raw responseHeaders', async (t) => { t.equal(infos.length, 1) t.same(infos[0].headers, ['Link', '; rel=preload; as=style']) t.same(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) - t.pass() + t.ok(true, 'pass') }) test('request formData', (t) => { @@ -793,7 +793,7 @@ test('request post body Buffer from string', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -823,7 +823,7 @@ test('request post body Buffer from buffer', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -853,7 +853,7 @@ test('request post body Uint8Array', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -883,7 +883,7 @@ test('request post body Uint32Array', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -913,7 +913,7 @@ test('request post body Float64Array', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -943,7 +943,7 @@ test('request post body BigUint64Array', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) @@ -973,6 +973,6 @@ test('request post body DataView', (t) => { maxRedirections: 2 }) await body.text() - t.pass() + t.ok(true, 'pass') }) }) diff --git a/test/client-stream.js b/test/client-stream.js index 7732187f159..a77804ace51 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -729,7 +729,7 @@ test('stream needDrain', (t) => { }) p.then(() => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -782,7 +782,7 @@ test('stream legacy needDrain', (t) => { }) p.then(() => { - t.pass() + t.ok(true, 'pass') }) }) diff --git a/test/client-timeout.js b/test/client-timeout.js index e02c46ceff3..38895c600c6 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -187,7 +187,7 @@ test('parser resume with no body timeout', (t) => { }, onComplete () { - t.pass() + t.ok(true, 'pass') }, onError (err) { t.error(err) diff --git a/test/client-upgrade.js b/test/client-upgrade.js index 80aa7314b30..84b84424e97 100644 --- a/test/client-upgrade.js +++ b/test/client-upgrade.js @@ -316,7 +316,7 @@ test('upgrade aborted', (t) => { signal.emit('abort') client.close(() => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index c511fb4fb68..237bb527f10 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -27,7 +27,7 @@ test('socket close listener does not leak', (t) => { const onRequest = (err, data) => { t.error(err) - data.body.on('end', () => t.pass()).resume() + data.body.on('end', () => t.ok(true, 'pass')).resume() } function onWarning (warning) { diff --git a/test/client.js b/test/client.js index 5aa031f6907..7f23ecd034d 100644 --- a/test/client.js +++ b/test/client.js @@ -469,7 +469,7 @@ test('basic head', (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) @@ -480,7 +480,7 @@ test('basic head', (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -509,7 +509,7 @@ test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) @@ -520,7 +520,7 @@ test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -611,7 +611,7 @@ test('head with host header', (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -1061,7 +1061,7 @@ test('basic POST with empty stream', (t) => { t.fail() }) .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -1124,7 +1124,7 @@ test('10 times HEAD', (t) => { body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) } @@ -1256,7 +1256,7 @@ test('multiple destroy callback', (t) => { data.body .resume() .on('error', () => { - t.pass() + t.ok(true, 'pass') }) client.destroy(new Error(), (err) => { t.error(err) @@ -1316,7 +1316,7 @@ test('only one streaming req at a time', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) t.equal(client[kBusy], true) @@ -1370,7 +1370,7 @@ test('only one async iterating req at a time', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) t.equal(client[kBusy], true) @@ -1399,7 +1399,7 @@ test('300 requests succeed', (t) => { data.body.on('data', (chunk) => { t.equal(chunk.toString(), 'asd') }).on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) } @@ -1650,7 +1650,7 @@ test('emit disconnect after destroy', t => { let disconnected = false client.on('disconnect', () => { disconnected = true - t.pass() + t.ok(true, 'pass') }) client.destroy(() => { t.equal(disconnected, true) @@ -1684,7 +1684,7 @@ test('end response before request', t => { t.fail() }) .on('end', () => { - t.pass() + t.ok(true, 'pass') }) .resume() client.on('disconnect', (url, targets, err) => { @@ -1808,7 +1808,7 @@ test('async iterator error from server destroys early', (t) => { yield 'inner-value' t.fail('should not get here, iterator should be destroyed') } finally { - t.ok(true) + t.ok(true, 'pass') } })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { @@ -1848,7 +1848,7 @@ test('regular iterator error from server closes early', (t) => { t.fail('should not get here, iterator should be destroyed') yield 'zzz' } finally { - t.ok(true) + t.ok(true, 'pass') } })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { @@ -1885,7 +1885,7 @@ test('async iterator early return closes early', (t) => { yield 'inner-value' t.fail('should not get here, iterator should be destroyed') } finally { - t.ok(true) + t.ok(true, 'pass') } })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { @@ -1916,7 +1916,7 @@ test('async iterator yield unsupported TypedArray', (t) => { yield new Int32Array([1]) t.fail('should not get here, iterator should be destroyed') } finally { - t.ok(true) + t.ok(true, 'pass') } })() client.request({ path: '/', method: 'POST', body }, (err) => { @@ -1946,7 +1946,7 @@ test('async iterator yield object error', (t) => { yield {} t.fail('should not get here, iterator should be destroyed') } finally { - t.ok(true) + t.ok(true, 'pass') } })() client.request({ path: '/', method: 'POST', body }, (err) => { diff --git a/test/close-and-destroy.js b/test/close-and-destroy.js index 6675e1d2cd0..3a949502494 100644 --- a/test/close-and-destroy.js +++ b/test/close-and-destroy.js @@ -11,7 +11,7 @@ test('close waits for queued requests to finish', (t) => { const server = createServer() server.on('request', (req, res) => { - t.pass('request received') + t.ok(true, 'request received') res.end('hello') }) t.teardown(server.close.bind(server)) diff --git a/test/connect-errconnect.js b/test/connect-errconnect.js index 885d6ddb19c..85b11f83847 100644 --- a/test/connect-errconnect.js +++ b/test/connect-errconnect.js @@ -11,7 +11,7 @@ test('connect-connectionError', t => { t.teardown(client.close.bind(client)) client.once('connectionError', () => { - t.pass() + t.ok(true, 'pass') }) const _err = new Error('kaboom') diff --git a/test/content-length.js b/test/content-length.js index 0257e4d62a4..aa5efb84167 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -109,9 +109,9 @@ function invalidContentLength (bodyType) { t.teardown(client.destroy.bind(client)) client.once('disconnect', () => { - t.pass() + t.ok(true, 'pass') client.once('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) @@ -217,7 +217,7 @@ test('request streaming no body data when content-length=0', (t) => { t.fail() }) .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -282,7 +282,7 @@ test('request streaming with Readable.from(buf)', (t) => { }) .on('end', () => { t.equal(Buffer.concat(chunks).toString(), 'hello') - t.pass() + t.ok(true, 'pass') t.end() }) }) @@ -330,7 +330,7 @@ test('request DELETE, content-length=0, with body', (t) => { }) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -439,7 +439,7 @@ test('content-length shouldSendContentLength=false', (t) => { }) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/get-head-body.js b/test/get-head-body.js index 9b587980224..74420a174cd 100644 --- a/test/get-head-body.js +++ b/test/get-head-body.js @@ -21,7 +21,7 @@ test('GET and HEAD with body should reset connection', (t) => { t.teardown(client.destroy.bind(client)) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) client.request({ @@ -145,7 +145,7 @@ test('HEAD should reset connection', (t) => { t.teardown(client.destroy.bind(client)) client.once('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) client.request({ @@ -172,7 +172,7 @@ test('HEAD should reset connection', (t) => { t.error(err) data.body.resume() data.body.on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) t.equal(client[kBusy], true) diff --git a/test/http-100.js b/test/http-100.js index add1273d9c0..434b53a5c2f 100644 --- a/test/http-100.js +++ b/test/http-100.js @@ -55,7 +55,7 @@ test('error 103 body', (t) => { t.equal(err.code, 'HPE_INVALID_CONSTANT') }) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -79,7 +79,7 @@ test('error 100 body', (t) => { t.equal(err.message, 'bad response') }) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) }) @@ -103,7 +103,7 @@ test('error 101 upgrade', (t) => { t.equal(err.message, 'bad upgrade') }) client.on('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/http2.js b/test/http2.js index 2daef09bd2c..33c2b72aa91 100644 --- a/test/http2.js +++ b/test/http2.js @@ -829,7 +829,7 @@ test('Should handle h2 request with body (string or buffer) - dispatch', t => { }, { onConnect () { - t.ok(true) + t.ok(true, 'pass') }, onError (err) { t.error(err) diff --git a/test/inflight-and-close.js b/test/inflight-and-close.js index d1930ff1bed..576a8c568ff 100644 --- a/test/inflight-and-close.js +++ b/test/inflight-and-close.js @@ -12,14 +12,14 @@ const server = http.createServer((req, res) => { const url = `http://127.0.0.1:${this.address().port}` request(url) .then(({ statusCode, headers, body }) => { - t.pass('first response') + t.ok(true, 'first response') body.resume() body.on('close', function () { - t.pass('first body closed') + t.ok(true, 'first body closed') }) return request(url) .then(({ statusCode, headers, body }) => { - t.pass('second response') + t.ok(true, 'second response') body.resume() body.on('close', function () { server.close() diff --git a/test/issue-810.js b/test/issue-810.js index ba4a35234e7..2640fd8d5c3 100644 --- a/test/issue-810.js +++ b/test/issue-810.js @@ -32,7 +32,7 @@ test('https://github.com/mcollina/undici/issues/810', (t) => { t.error(err) data.body.resume().on('end', () => { // t.fail() FIX: Should fail. - t.pass() + t.ok(true, 'pass') }).on('error', err => ( t.type(err, errors.HTTPParserError) )) @@ -66,7 +66,7 @@ test('https://github.com/mcollina/undici/issues/810 no pipelining', (t) => { t.error(err) data.body.resume().on('end', () => { // t.fail() FIX: Should fail. - t.pass() + t.ok(true, 'pass') }) }) }) @@ -93,7 +93,7 @@ test('https://github.com/mcollina/undici/issues/810 pipelining', (t) => { t.error(err) data.body.resume().on('end', () => { // t.fail() FIX: Should fail. - t.pass() + t.ok(true, 'pass') }) }) }) @@ -120,7 +120,7 @@ test('https://github.com/mcollina/undici/issues/810 pipelining 2', (t) => { t.error(err) data.body.resume().on('end', () => { // t.fail() FIX: Should fail. - t.pass() + t.ok(true, 'pass') }) }) diff --git a/test/max-headers.js b/test/max-headers.js index 9e7396ae76f..a7946e81098 100644 --- a/test/max-headers.js +++ b/test/max-headers.js @@ -34,7 +34,7 @@ test('handle a lot of headers', (t) => { data.body .resume() .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/node-test/client-dispatch.js b/test/node-test/client-dispatch.js index b89bad7278b..d780da3b7b9 100644 --- a/test/node-test/client-dispatch.js +++ b/test/node-test/client-dispatch.js @@ -387,7 +387,7 @@ test('connect call onUpgrade once', async (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.pass('should not throw') + t.ok(true, 'should not throw') }, onUpgrade (statusCode, headers, socket) { p.strictEqual(count++, 0) @@ -435,13 +435,13 @@ test('dispatch onConnect missing', async (t) => { method: 'GET' }, { onHeaders (statusCode, headers) { - t.pass('should not throw') + t.ok(true, 'should not throw') }, onData (buf) { - t.pass('should not throw') + t.ok(true, 'should not throw') }, onComplete (trailers) { - t.pass('should not throw') + t.ok(true, 'should not throw') }, onError (err) { p.strictEqual(err.code, 'UND_ERR_INVALID_ARG') diff --git a/test/parser-issues.js b/test/parser-issues.js index c94e8cf04bc..aef7c42664e 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -32,7 +32,7 @@ test('https://github.com/mcollina/undici/issues/268', (t) => { data.body .resume() setTimeout(() => { - t.pass() + t.ok(true, 'pass') data.body.on('error', () => {}) }, 2e3) }) diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index 4a94e38375f..d3f7143cba4 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -65,7 +65,7 @@ test('pipeline pipelining retry', (t) => { t.teardown(client.destroy.bind(client)) client.once('disconnect', () => { - t.pass() + t.ok(true, 'pass') }) client[kConnect](() => { @@ -101,7 +101,7 @@ test('pipeline pipelining retry', (t) => { }) client.close(() => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/pool.js b/test/pool.js index 0d586655b9f..b1c8722913e 100644 --- a/test/pool.js +++ b/test/pool.js @@ -423,10 +423,10 @@ test('busy', (t) => { pipelining: 2 }) client.on('drain', () => { - t.pass() + t.ok(true, 'pass') }) client.on('connect', () => { - t.pass() + t.ok(true, 'pass') }) t.teardown(client.destroy.bind(client)) @@ -623,7 +623,7 @@ test('300 requests succeed', (t) => { data.body.on('data', (chunk) => { t.equal(chunk.toString(), 'asd') }).on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) } @@ -716,7 +716,7 @@ test('pool dispatch error', (t) => { onData (chunk) { }, onComplete () { - t.pass() + t.ok(true, 'pass') }, onError () { } @@ -772,7 +772,7 @@ test('pool request abort in queue', (t) => { onData (chunk) { }, onComplete () { - t.pass() + t.ok(true, 'pass') }, onError () { } @@ -817,7 +817,7 @@ test('pool stream abort in queue', (t) => { onData (chunk) { }, onComplete () { - t.pass() + t.ok(true, 'pass') }, onError () { } @@ -862,7 +862,7 @@ test('pool pipeline abort in queue', (t) => { onData (chunk) { }, onComplete () { - t.pass() + t.ok(true, 'pass') }, onError () { } @@ -1014,11 +1014,11 @@ test('pool close waits for all requests', (t) => { }) client.close(() => { - t.pass() + t.ok(true, 'pass') }) client.close(() => { - t.pass() + t.ok(true, 'pass') }) client.request({ @@ -1087,7 +1087,7 @@ test('pool destroy fails queued requests', (t) => { t.equal(client.destroyed, false) client.destroy(_err, () => { - t.pass() + t.ok(true, 'pass') }) t.equal(client.destroyed, true) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 806f02a2d55..70e76756a54 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -46,7 +46,7 @@ test('use proxy-agent to connect through proxy', async (t) => { const parsedOrigin = new URL(serverUrl) proxy.on('connect', () => { - t.pass('should connect to proxy') + t.ok(true, 'should connect to proxy') }) server.on('request', (req, res) => { @@ -81,7 +81,7 @@ test('use proxy agent to connect through proxy using Pool', async (t) => { proxy.authenticate = async function (req, fn) { if (++connectCount === 2) { - t.pass('second connect should arrive while first is still inflight') + t.ok(true, 'second connect should arrive while first is still inflight') resolveFirstConnect() fn(null, true) } else { @@ -122,7 +122,7 @@ test('use proxy-agent to connect through proxy using path with params', async (t const parsedOrigin = new URL(serverUrl) proxy.on('connect', () => { - t.pass('should call proxy') + t.ok(true, 'should call proxy') }) server.on('request', (req, res) => { t.equal(req.url, '/hello?foo=bar') @@ -161,11 +161,11 @@ test('use proxy-agent with auth', async (t) => { const parsedOrigin = new URL(serverUrl) proxy.authenticate = function (req, fn) { - t.pass('authentication should be called') + t.ok(true, 'authentication should be called') fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) } proxy.on('connect', () => { - t.pass('proxy should be called') + t.ok(true, 'proxy should be called') }) server.on('request', (req, res) => { @@ -205,11 +205,11 @@ test('use proxy-agent with token', async (t) => { const parsedOrigin = new URL(serverUrl) proxy.authenticate = function (req, fn) { - t.pass('authentication should be called') + t.ok(true, 'authentication should be called') fn(null, req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`) } proxy.on('connect', () => { - t.pass('proxy should be called') + t.ok(true, 'proxy should be called') }) server.on('request', (req, res) => { @@ -341,7 +341,7 @@ test('use proxy-agent with setGlobalDispatcher', async (t) => { t.teardown(() => setGlobalDispatcher(defaultDispatcher)) proxy.on('connect', () => { - t.pass('should call proxy') + t.ok(true, 'should call proxy') }) server.on('request', (req, res) => { t.equal(req.url, '/hello?foo=bar') @@ -432,7 +432,7 @@ test('should throw when proxy does not return 200', async (t) => { await request(serverUrl, { dispatcher: proxyAgent }) t.fail() } catch (e) { - t.pass() + t.ok(true, 'pass') t.ok(e) } @@ -492,7 +492,7 @@ test('Proxy via HTTP to HTTPS endpoint', async (t) => { }) server.on('secureConnection', () => { - t.pass('server should be connected secured') + t.ok(true, 'server should be connected secured') }) proxy.on('secureConnection', () => { @@ -500,7 +500,7 @@ test('Proxy via HTTP to HTTPS endpoint', async (t) => { }) proxy.on('connect', function () { - t.pass('proxy should be connected') + t.ok(true, 'proxy should be connected') }) proxy.on('request', function () { @@ -553,15 +553,15 @@ test('Proxy via HTTPS to HTTPS endpoint', async (t) => { }) server.on('secureConnection', () => { - t.pass('server should be connected secured') + t.ok(true, 'server should be connected secured') }) proxy.on('secureConnection', () => { - t.pass('proxy over http should call secureConnection') + t.ok(true, 'proxy over http should call secureConnection') }) proxy.on('connect', function () { - t.pass('proxy should be connected') + t.ok(true, 'proxy should be connected') }) proxy.on('request', function () { @@ -610,7 +610,7 @@ test('Proxy via HTTPS to HTTP endpoint', async (t) => { }) proxy.on('secureConnection', () => { - t.pass('proxy over http should call secureConnection') + t.ok(true, 'proxy over http should call secureConnection') }) proxy.on('request', function () { @@ -652,7 +652,7 @@ test('Proxy via HTTP to HTTP endpoint', async (t) => { }) proxy.on('connect', () => { - t.pass('connect to proxy') + t.ok(true, 'connect to proxy') }) proxy.on('request', function () { diff --git a/test/redirect-request.js b/test/redirect-request.js index da77aa78502..d200ec17aa8 100644 --- a/test/redirect-request.js +++ b/test/redirect-request.js @@ -284,7 +284,7 @@ for (const factory of [ t.equal(body.length, 0) } catch (error) { if (error.message.startsWith('max redirects')) { - t.pass('Max redirects handled correctly') + t.ok(true, 'Max redirects handled correctly') } else { t.fail(`Unexpected error: ${error.message}`) } diff --git a/test/retry-handler.js b/test/retry-handler.js index d90ff5ebc45..b4000606f62 100644 --- a/test/retry-handler.js +++ b/test/retry-handler.js @@ -60,10 +60,10 @@ tap.test('Should retry status code', t => { dispatch: client.dispatch.bind(client), handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 200) @@ -145,10 +145,10 @@ tap.test('Should use retry-after header for retries', t => { dispatch: client.dispatch.bind(client), handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 200) @@ -231,10 +231,10 @@ tap.test('Should use retry-after header for retries (date)', t => { dispatch: client.dispatch.bind(client), handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 200) @@ -314,10 +314,10 @@ tap.test('Should retry with defaults', t => { dispatch: client.dispatch.bind(client), handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 200) @@ -364,7 +364,7 @@ tap.test('Should handle 206 partial content', t => { let x = 0 const server = createServer((req, res) => { if (x === 0) { - t.pass() + t.ok(true, 'pass') res.setHeader('etag', 'asd') res.write('abc') setTimeout(() => { @@ -411,13 +411,13 @@ tap.test('Should handle 206 partial content', t => { }, handler: { onRequestSent () { - t.pass() + t.ok(true, 'pass') }, onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 200) @@ -464,7 +464,7 @@ tap.test('Should handle 206 partial content - bad-etag', t => { let x = 0 const server = createServer((req, res) => { if (x === 0) { - t.pass() + t.ok(true, 'pass') res.setHeader('etag', 'asd') res.write('abc') setTimeout(() => { @@ -500,13 +500,13 @@ tap.test('Should handle 206 partial content - bad-etag', t => { }, handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.pass() + t.ok(true, 'pass') return true }, onData (chunk) { @@ -647,10 +647,10 @@ tap.test('should not error if request is not meant to be retried', t => { dispatch: client.dispatch.bind(client), handler: { onConnect () { - t.pass() + t.ok(true, 'pass') }, onBodySent () { - t.pass() + t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { t.equal(status, 400) diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index 325ac993933..bdc2c74346f 100644 --- a/test/socket-back-pressure.js +++ b/test/socket-back-pressure.js @@ -47,7 +47,7 @@ test('socket back-pressure', (t) => { }, 1e3) }) .on('end', () => { - t.pass() + t.ok(true, 'pass') }) }) }) diff --git a/test/socket-timeout.js b/test/socket-timeout.js index eb9670c6020..f2ad9faa34a 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -12,7 +12,7 @@ test('timeout with pipelining 1', (t) => { const server = createServer() server.once('request', (req, res) => { - t.pass('first request received, we are letting this timeout on the client') + t.ok(true, 'first request received, we are letting this timeout on the client') server.once('request', (req, res) => { t.equal('/', req.url) diff --git a/test/tls-session-reuse.js b/test/tls-session-reuse.js index ae65571bfa2..0c784456798 100644 --- a/test/tls-session-reuse.js +++ b/test/tls-session-reuse.js @@ -70,7 +70,7 @@ test('A client should disable session caching', t => { if (queue.length !== 0) { return request() } - t.pass() + t.ok(true, 'pass') }) }) } @@ -170,7 +170,7 @@ test('A pool should be able to reuse TLS sessions between clients', t => { t.equal(numSessions, 2) t.equal(serverRequests, 2 + REQ_COUNT * 2) - t.pass() + t.ok(true, 'pass') }) }) diff --git a/test/tls.js b/test/tls.js index cd4660fa319..8c2b4cc8afb 100644 --- a/test/tls.js +++ b/test/tls.js @@ -22,7 +22,7 @@ // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // }) // }) @@ -45,7 +45,7 @@ // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // }) // }) @@ -71,10 +71,10 @@ // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // client.once('disconnect', () => { -// t.pass() +// t.ok(true, 'pass') // didDisconnect = true // }) // }) @@ -133,14 +133,14 @@ // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // }) // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // }) // }) @@ -166,10 +166,10 @@ // data.body // .resume() // .on('end', () => { -// t.pass() +// t.ok(true, 'pass') // }) // client.once('disconnect', () => { -// t.pass() +// t.ok(true, 'pass') // didDisconnect = true // }) // }) From c4adfddf09a2c2118fdc1c2bf8b25298056314a8 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:49:54 +0900 Subject: [PATCH 008/123] perf: remove redundant operation in FormData (#2726) --- lib/core/util.js | 8 +------- lib/fetch/formdata.js | 11 ++++------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 55bf9f49822..7b863067870 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -438,13 +438,7 @@ const hasToWellFormed = !!String.prototype.toWellFormed * @param {string} val */ function toUSVString (val) { - if (hasToWellFormed) { - return `${val}`.toWellFormed() - } else if (nodeUtil.toUSVString) { - return nodeUtil.toUSVString(val) - } - - return `${val}` + return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val) } /** diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index b519f890b18..add64aa9226 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -1,6 +1,6 @@ 'use strict' -const { isBlobLike, toUSVString, makeIterator } = require('./util') +const { isBlobLike, makeIterator } = require('./util') const { kState } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { File: UndiciFile, FileLike, isFileLike } = require('./file') @@ -133,7 +133,7 @@ class FormData { ? webidl.converters.Blob(value, { strict: false }) : webidl.converters.USVString(value) filename = arguments.length === 3 - ? toUSVString(filename) + ? webidl.converters.USVString(filename) : undefined // 2. Let entry be the result of creating an entry with name, value, and @@ -238,15 +238,12 @@ Object.defineProperties(FormData.prototype, { */ function makeEntry (name, value, filename) { // 1. Set name to the result of converting name into a scalar value string. - // "To convert a string into a scalar value string, replace any surrogates - // with U+FFFD." - // see: https://nodejs.org/dist/latest-v20.x/docs/api/buffer.html#buftostringencoding-start-end - name = Buffer.from(name).toString('utf8') + // Note: This operation was done by the webidl converter USVString. // 2. If value is a string, then set value to the result of converting // value into a scalar value string. if (typeof value === 'string') { - value = Buffer.from(value).toString('utf8') + // Note: This operation was done by the webidl converter USVString. } else { // 3. Otherwise: From c2f006894c53609032ccbf82a98e1b0221da4d66 Mon Sep 17 00:00:00 2001 From: Maksym Shenderuk Date: Fri, 9 Feb 2024 20:40:56 +0300 Subject: [PATCH 009/123] Add support for passing iterable objects as headers (#2708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update tests with iterable object cases * Add support for iterable object headers * Update tests with cases of malformed headers * Add check for malformed headers * Update lib/core/request.js Co-authored-by: Mert Can Altın * Update lib/core/request.js Co-authored-by: Mert Can Altın * Fix code after unverified improvement broke functionality --------- Co-authored-by: Mert Can Altın --- lib/core/request.js | 17 ++++-- test/request.js | 135 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 74e0ca16eaa..bee7a47af92 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -164,10 +164,19 @@ class Request { processHeader(this, headers[i], headers[i + 1]) } } else if (headers && typeof headers === 'object') { - const keys = Object.keys(headers) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - processHeader(this, key, headers[key]) + if (headers[Symbol.iterator]) { + for (const header of headers) { + if (!Array.isArray(header) || header.length !== 2) { + throw new InvalidArgumentError('headers must be in key-value pair format') + } + const [key, value] = header + processHeader(this, key, value) + } + } else { + const keys = Object.keys(headers) + for (const key of keys) { + processHeader(this, key, headers[key]) + } } } else if (headers != null) { throw new InvalidArgumentError('headers must be an object or an array') diff --git a/test/request.js b/test/request.js index 30bf745c3f1..bbfab5b3f5c 100644 --- a/test/request.js +++ b/test/request.js @@ -2,7 +2,7 @@ const { createServer } = require('node:http') const { test } = require('tap') -const { request } = require('..') +const { request, errors } = require('..') test('no-slash/one-slash pathname should be included in req.path', async (t) => { const pathServer = createServer((req, res) => { @@ -246,3 +246,136 @@ test('DispatchOptions#reset', scope => { }) }) }) + +test('Should include headers from iterable objects', scope => { + scope.plan(4) + + scope.test('Should include headers built with Headers global object', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = new Headers() + headers.set('hello', 'world') + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should include headers built with Map', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = new Map() + headers.set('hello', 'world') + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should include headers built with custom iterable object', async t => { + const server = createServer((req, res) => { + t.equal('GET', req.method) + t.equal(`localhost:${server.address().port}`, req.headers.host) + t.equal(req.headers.hello, 'world') + res.statusCode = 200 + res.end('hello') + }) + + const headers = { + * [Symbol.iterator] () { + yield ['hello', 'world'] + } + } + + t.plan(3) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }) + }) + + scope.test('Should throw error if headers iterable object does not yield key-value pairs', async t => { + const server = createServer((req, res) => { + res.end('hello') + }) + + const headers = { + * [Symbol.iterator] () { + yield 'Bad formatted header' + } + } + + t.plan(2) + + t.teardown(server.close.bind(server)) + + await new Promise((resolve, reject) => { + server.listen(0, (err) => { + if (err != null) reject(err) + else resolve() + }) + }) + + await request({ + method: 'GET', + origin: `http://localhost:${server.address().port}`, + reset: true, + headers + }).catch((err) => { + t.type(err, errors.InvalidArgumentError) + t.equal(err.message, 'headers must be in key-value pair format') + }) + }) +}) From 08607353423e04b94da135398a1cc485c550a906 Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Sat, 10 Feb 2024 01:41:56 +0800 Subject: [PATCH 010/123] chore: refine esbuild & node detection (#2677) When using the loader for external builtins, `esbuildDetection` is undefined. This commit defines `__UNDICI_IS_NODE__` on `globalThis` in the loader and deletes it after loading Undici. `esbuildDetection` has also been extracted as a variable at the top level of the module, to support deleting `__UNDICI_IS_NODE__` on `globalThis` to avoid polluting the global namespace. --- build/wasm.js | 2 ++ lib/fetch/index.js | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build/wasm.js b/build/wasm.js index 3fb8f103117..ca89ec7d4cf 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -101,6 +101,8 @@ if (EXTERNAL_PATH) { writeFileSync(join(ROOT, 'loader.js'), ` 'use strict' +globalThis.__UNDICI_IS_NODE__ = true module.exports = require('node:module').createRequire('${EXTERNAL_PATH}/loader.js')('./index-fetch.js') +delete globalThis.__UNDICI_IS_NODE__ `) } diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 4b89b4a48e3..a11f0a8d506 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -66,6 +66,10 @@ const { webidl } = require('./webidl') const { STATUS_CODES } = require('node:http') const GET_OR_HEAD = ['GET', 'HEAD'] +const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined' + ? 'node' + : 'undici' + /** @type {import('buffer').resolveObjectURL} */ let resolveObjectURL @@ -1478,7 +1482,7 @@ async function httpNetworkOrCacheFetch ( // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. if (!httpRequest.headersList.contains('user-agent', true)) { - httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node', true) + httpRequest.headersList.append('user-agent', defaultUserAgent) } // 15. If httpRequest’s cache mode is "default" and httpRequest’s header From cebfdcddf46ce190a5be003366845adc7009adcb Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 9 Feb 2024 18:45:50 +0100 Subject: [PATCH 011/123] rephrase some comments (#2717) --- lib/fetch/index.js | 2 +- lib/fetch/util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index a11f0a8d506..a989295bc4b 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -369,7 +369,7 @@ function fetching ({ useParallelQueue = false, dispatcher // undici }) { - // This has bitten me in the ass more times than I'd like to admit. + // Ensure that the dispatcher is set accordingly assert(dispatcher) // 1. Let taskDestination be null. diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 8812895bf4d..c5a6b46b170 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -1072,7 +1072,7 @@ function simpleRangeHeaderValue (value, allowWhitespace) { // 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab // or space, from data given position. - // Note from Khafra: its the same fucking step again lol + // Note from Khafra: its the same step as in #8 again lol if (allowWhitespace) { collectASequenceOfCodePoints( (char) => char === '\t' || char === ' ', From 50d691d366729433ae4f59b2950e755f31c3661e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 9 Feb 2024 23:39:19 +0100 Subject: [PATCH 012/123] test: replace t.type with t.ok (#2720) --- test/client-connect.js | 2 +- test/client-pipeline.js | 26 ++++++------- test/client-request.js | 4 +- test/client-stream.js | 20 +++++----- test/client-timeout.js | 6 +-- test/client-upgrade.js | 16 ++++---- test/client.js | 6 +-- test/close-and-destroy.js | 16 ++++---- test/connect-timeout.js | 4 +- test/content-length.js | 22 +++++------ test/headers-as-array.js | 2 +- test/http2.js | 2 +- test/invalid-headers.js | 20 +++++----- test/issue-810.js | 6 +-- test/max-response-size.js | 2 +- test/mock-agent.js | 14 +++---- test/mock-client.js | 16 ++++---- test/mock-errors.js | 4 +- test/mock-interceptor.js | 14 +++---- test/mock-pool.js | 8 ++-- test/mock-scope.js | 6 +-- test/parser-issues.js | 2 +- test/pool.js | 24 ++++++------ test/request-crlf.js | 2 +- test/request-timeout.js | 50 ++++++++++++------------- test/socket-timeout.js | 2 +- test/utils/esm-wrapper.mjs | 6 +-- test/wpt/tests/resources/testharness.js | 2 +- 28 files changed, 152 insertions(+), 152 deletions(-) diff --git a/test/client-connect.js b/test/client-connect.js index 587361f9ef0..4917a526d0e 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -31,7 +31,7 @@ test('connect aborted after connect', (t) => { opaque: 'asd' }, (err, { opaque }) => { t.equal(opaque, 'asd') - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) t.equal(client[kBusy], true) }) diff --git a/test/client-pipeline.js b/test/client-pipeline.js index e37657456ff..cbc78b9270d 100644 --- a/test/client-pipeline.js +++ b/test/client-pipeline.js @@ -340,7 +340,7 @@ test('pipeline invalid handler return', (t) => { body.on('error', () => {}) }) .on('error', (err) => { - t.type(err, errors.InvalidReturnValueError) + t.ok(err instanceof errors.InvalidReturnValueError) }) .end() @@ -353,7 +353,7 @@ test('pipeline invalid handler return', (t) => { return {} }) .on('error', (err) => { - t.type(err, errors.InvalidReturnValueError) + t.ok(err instanceof errors.InvalidReturnValueError) }) .end() }) @@ -409,7 +409,7 @@ test('pipeline destroy and throw handler', (t) => { }) .end() .on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) .on('close', () => { t.ok(true, 'pass') @@ -449,7 +449,7 @@ test('pipeline abort res', (t) => { return body }) .on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) .end() }) @@ -474,7 +474,7 @@ test('pipeline abort server res', (t) => { t.fail() }) .on('error', (err) => { - t.type(err, errors.SocketError) + t.ok(err instanceof errors.SocketError) }) .end() }) @@ -505,7 +505,7 @@ test('pipeline abort duplex', (t) => { }, () => { t.fail() }).destroy().on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) }) }) @@ -558,7 +558,7 @@ test('pipeline abort piped res 2', (t) => { }, ({ body }) => { const pt = new PassThrough() body.on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) setImmediate(() => { pt.destroy() @@ -567,7 +567,7 @@ test('pipeline abort piped res 2', (t) => { return pt }) .on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) .end() }) @@ -628,7 +628,7 @@ test('pipeline abort server res after headers', (t) => { return data.body }) .on('error', (err) => { - t.type(err, errors.SocketError) + t.ok(err instanceof errors.SocketError) }) .end() }) @@ -656,7 +656,7 @@ test('pipeline w/ write abort server res after headers', (t) => { return data.body }) .on('error', (err) => { - t.type(err, errors.SocketError) + t.ok(err instanceof errors.SocketError) }) .resume() .write('asd') @@ -713,7 +713,7 @@ test('pipeline args validation', (t) => { const ret = client.pipeline(null, () => {}) ret.on('error', (err) => { t.ok(/opts/.test(err.message)) - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) @@ -866,7 +866,7 @@ test('pipeline CONNECT throw', (t) => { }, () => { t.fail() }).on('error', (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.on('disconnect', () => { t.fail() @@ -1030,7 +1030,7 @@ test('pipeline abort after headers', (t) => { }) .end() .on('error', (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) }) }) diff --git a/test/client-request.js b/test/client-request.js index aa9405b2fcc..cafcd39989b 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -117,7 +117,7 @@ test('request abort before headers', (t) => { method: 'GET', signal }, (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) t.equal(signal.listenerCount('abort'), 0) }) t.equal(signal.listenerCount('abort'), 1) @@ -127,7 +127,7 @@ test('request abort before headers', (t) => { method: 'GET', signal }, (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) t.equal(signal.listenerCount('abort'), 0) }) t.equal(signal.listenerCount('abort'), 2) diff --git a/test/client-stream.js b/test/client-stream.js index a77804ace51..69843acf0df 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -263,17 +263,17 @@ test('stream args validation', (t) => { path: '/', method: 'GET' }, null, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.stream(null, null, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) try { client.stream(null, null, 'asd') } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } }) @@ -285,11 +285,11 @@ test('stream args validation promise', (t) => { path: '/', method: 'GET' }, null).catch((err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.stream(null, null).catch((err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) @@ -337,7 +337,7 @@ test('stream server side destroy', (t) => { }, () => { t.fail() }, (err) => { - t.type(err, errors.SocketError) + t.ok(err instanceof errors.SocketError) }) }) }) @@ -360,7 +360,7 @@ test('stream invalid return', (t) => { }, () => { return {} }, (err) => { - t.type(err, errors.InvalidReturnValueError) + t.ok(err instanceof errors.InvalidReturnValueError) }) }) }) @@ -413,7 +413,7 @@ test('stream factory abort', (t) => { return new PassThrough() }, (err) => { t.equal(signal.listenerCount('abort'), 0) - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) t.equal(signal.listenerCount('abort'), 1) }) @@ -475,7 +475,7 @@ test('stream CONNECT throw', (t) => { method: 'CONNECT' }, () => { }, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) }) @@ -528,7 +528,7 @@ test('stream abort before dispatch', (t) => { }, () => { return pt }, (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) signal.emit('abort') }) diff --git a/test/client-timeout.js b/test/client-timeout.js index 38895c600c6..dd5b1da6505 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -40,7 +40,7 @@ test('refresh timeout on pause', (t) => { }, onError (err) { - t.type(err, errors.BodyTimeoutError) + t.ok(err instanceof errors.BodyTimeoutError) } }) }) @@ -96,7 +96,7 @@ test('start headers timeout after request body', (t) => { }, onError (err) { t.equal(body.readableEnded, true) - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) } }) }) @@ -153,7 +153,7 @@ test('start headers timeout after async iterator request body', (t) => { }, onError (err) { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) } }) }) diff --git a/test/client-upgrade.js b/test/client-upgrade.js index 84b84424e97..c33ab183363 100644 --- a/test/client-upgrade.js +++ b/test/client-upgrade.js @@ -151,7 +151,7 @@ test('upgrade invalid opts', (t) => { const client = new Client('http://localhost:5432') client.upgrade(null, err => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) t.equal(err.message, 'invalid opts') }) @@ -159,7 +159,7 @@ test('upgrade invalid opts', (t) => { client.upgrade(null, null) t.fail() } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) t.equal(err.message, 'invalid opts') } @@ -167,7 +167,7 @@ test('upgrade invalid opts', (t) => { client.upgrade({ path: '/' }, null) t.fail() } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) t.equal(err.message, 'invalid callback') } }) @@ -308,7 +308,7 @@ test('upgrade aborted', (t) => { opaque: 'asd' }, (err, { opaque }) => { t.equal(opaque, 'asd') - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) t.equal(signal.listenerCount('abort'), 0) }) t.equal(client[kBusy], true) @@ -351,7 +351,7 @@ test('basic aborted after res', (t) => { protocol: 'Websocket', signal }, (err) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) }) }) }) @@ -408,7 +408,7 @@ test('upgrade disconnect', (t) => { client.on('disconnect', (origin, [self], error) => { t.equal(client, self) - t.type(error, Error) + t.ok(error instanceof Error) }) client @@ -417,7 +417,7 @@ test('upgrade disconnect', (t) => { t.fail() }) .catch(error => { - t.type(error, Error) + t.ok(error instanceof Error) }) }) }) @@ -446,7 +446,7 @@ test('upgrade invalid signal', (t) => { opaque: 'asd' }, (err, { opaque }) => { t.equal(opaque, 'asd') - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) }) diff --git a/test/client.js b/test/client.js index 7f23ecd034d..7c3c8d0fcb3 100644 --- a/test/client.js +++ b/test/client.js @@ -1412,13 +1412,13 @@ test('request args validation', (t) => { const client = new Client('http://localhost:5000') client.request(null, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) try { client.request(null, 'asd') } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } }) @@ -1428,7 +1428,7 @@ test('request args validation promise', (t) => { const client = new Client('http://localhost:5000') client.request(null).catch((err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) diff --git a/test/close-and-destroy.js b/test/close-and-destroy.js index 3a949502494..b946e3e18b6 100644 --- a/test/close-and-destroy.js +++ b/test/close-and-destroy.js @@ -72,10 +72,10 @@ test('destroy invoked all pending callbacks', (t) => { client.destroy() }) client.request({ path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) client.request({ path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -99,11 +99,11 @@ test('destroy invoked all pending callbacks ticked', (t) => { let ticked = false client.request({ path: '/', method: 'GET' }, (err) => { t.equal(ticked, true) - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) client.request({ path: '/', method: 'GET' }, (err) => { t.equal(ticked, true) - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) client.destroy() ticked = true @@ -223,10 +223,10 @@ test('closed and destroyed errors', (t) => { t.error(err) }) client.request({ path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientClosedError) + t.ok(err instanceof errors.ClientClosedError) client.destroy() client.request({ path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -239,10 +239,10 @@ test('close after and destroy should error', (t) => { client.destroy() client.close((err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) client.close().catch((err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index 9c001a35d97..5eab8ee1098 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -41,7 +41,7 @@ test('connect-timeout', t => { path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ConnectTimeoutError) + t.ok(err instanceof errors.ConnectTimeoutError) clearTimeout(timeout) }) }) @@ -62,7 +62,7 @@ test('connect-timeout', t => { path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ConnectTimeoutError) + t.ok(err instanceof errors.ConnectTimeoutError) clearTimeout(timeout) }) }) diff --git a/test/content-length.js b/test/content-length.js index aa5efb84167..6c124ed3530 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -25,7 +25,7 @@ test('request invalid content-length', (t) => { }, body: 'asd' }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -36,7 +36,7 @@ test('request invalid content-length', (t) => { }, body: 'asdasdasdasdasdasda' }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -47,7 +47,7 @@ test('request invalid content-length', (t) => { }, body: Buffer.alloc(9) }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -58,7 +58,7 @@ test('request invalid content-length', (t) => { }, body: Buffer.alloc(11) }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -69,7 +69,7 @@ test('request invalid content-length', (t) => { }, body: ['asd'] }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -80,7 +80,7 @@ test('request invalid content-length', (t) => { }, body: ['asasdasdasdd'] }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -91,7 +91,7 @@ test('request invalid content-length', (t) => { }, body: ['asasdasdasdd'] }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) }) @@ -130,7 +130,7 @@ function invalidContentLength (bodyType) { } }), bodyType) }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ @@ -148,7 +148,7 @@ function invalidContentLength (bodyType) { } }), bodyType) }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) }) @@ -184,7 +184,7 @@ function zeroContentLength (bodyType) { } }), bodyType) }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) }) @@ -315,7 +315,7 @@ test('request DELETE, content-length=0, with body', (t) => { } }) }, (err) => { - t.type(err, errors.RequestContentLengthMismatchError) + t.ok(err instanceof errors.RequestContentLengthMismatchError) }) client.request({ diff --git a/test/headers-as-array.js b/test/headers-as-array.js index 315d55b4b64..e50605332e5 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -103,7 +103,7 @@ test('fail if headers array is odd', (t) => { method: 'GET', headers }, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) t.equal(err.message, 'headers array must be even') }) }) diff --git a/test/http2.js b/test/http2.js index 33c2b72aa91..538c6677788 100644 --- a/test/http2.js +++ b/test/http2.js @@ -273,7 +273,7 @@ test('Should support H2 GOAWAY (server-side)', async t => { const [url, disconnectClient, err] = await once(client, 'disconnect') - t.type(url, URL) + t.ok(url instanceof URL) t.same(disconnectClient, [client]) t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204') }) diff --git a/test/invalid-headers.js b/test/invalid-headers.js index caf9e0ae66a..0c011e97ac1 100644 --- a/test/invalid-headers.js +++ b/test/invalid-headers.js @@ -15,7 +15,7 @@ test('invalid headers', (t) => { 'content-length': 'asd' } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -23,7 +23,7 @@ test('invalid headers', (t) => { method: 'GET', headers: 1 }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -33,7 +33,7 @@ test('invalid headers', (t) => { 'transfer-encoding': 'chunked' } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -43,7 +43,7 @@ test('invalid headers', (t) => { upgrade: 'asd' } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -53,7 +53,7 @@ test('invalid headers', (t) => { connection: 'asd' } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -63,7 +63,7 @@ test('invalid headers', (t) => { 'keep-alive': 'timeout=5' } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -73,7 +73,7 @@ test('invalid headers', (t) => { foo: {} } }, (err, data) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) client.request({ @@ -83,7 +83,7 @@ test('invalid headers', (t) => { expect: '100-continue' } }, (err, data) => { - t.type(err, errors.NotSupportedError) + t.ok(err instanceof errors.NotSupportedError) }) client.request({ @@ -93,7 +93,7 @@ test('invalid headers', (t) => { Expect: '100-continue' } }, (err, data) => { - t.type(err, errors.NotSupportedError) + t.ok(err instanceof errors.NotSupportedError) }) client.request({ @@ -103,6 +103,6 @@ test('invalid headers', (t) => { expect: 'asd' } }, (err, data) => { - t.type(err, errors.NotSupportedError) + t.ok(err instanceof errors.NotSupportedError) }) }) diff --git a/test/issue-810.js b/test/issue-810.js index 2640fd8d5c3..e18656c5869 100644 --- a/test/issue-810.js +++ b/test/issue-810.js @@ -34,14 +34,14 @@ test('https://github.com/mcollina/undici/issues/810', (t) => { // t.fail() FIX: Should fail. t.ok(true, 'pass') }).on('error', err => ( - t.type(err, errors.HTTPParserError) + t.ok(err instanceof errors.HTTPParserError) )) }) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.type(err, errors.HTTPParserError) + t.ok(err instanceof errors.HTTPParserError) }) }) }) @@ -129,7 +129,7 @@ test('https://github.com/mcollina/undici/issues/810 pipelining 2', (t) => { method: 'GET' }, (err, data) => { t.equal(err.code, 'HPE_INVALID_CONSTANT') - t.type(err, errors.HTTPParserError) + t.ok(err instanceof errors.HTTPParserError) }) }) }) diff --git a/test/max-response-size.js b/test/max-response-size.js index ca6ad40fa42..adf75c6fee2 100644 --- a/test/max-response-size.js +++ b/test/max-response-size.js @@ -84,7 +84,7 @@ test('max response size', (t) => { t.error(err) body.on('error', (err) => { t.ok(err) - t.type(err, errors.ResponseExceededMaxSizeError) + t.ok(err instanceof errors.ResponseExceededMaxSizeError) }) }) }) diff --git a/test/mock-agent.js b/test/mock-agent.js index 42dfb9554cc..ed58464b527 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -26,7 +26,7 @@ test('MockAgent - constructor', t => { t.plan(1) const mockAgent = new MockAgent() - t.type(mockAgent, Dispatcher) + t.ok(mockAgent instanceof Dispatcher) }) t.test('sets up mock agent with single connection', t => { @@ -62,7 +62,7 @@ test('MockAgent - get', t => { t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) }) t.test('should return MockPool', (t) => { @@ -74,7 +74,7 @@ test('MockAgent - get', t => { t.teardown(mockAgent.close.bind(mockAgent)) const mockPool = mockAgent.get(baseUrl) - t.type(mockPool, MockPool) + t.ok(mockPool instanceof MockPool) }) t.test('should return the same instance if already created', (t) => { @@ -241,7 +241,7 @@ test('MockAgent - .close should clean up registered pools', async (t) => { // Register a pool const mockPool = mockAgent.get(baseUrl) - t.type(mockPool, MockPool) + t.ok(mockPool instanceof MockPool) t.equal(mockPool[kConnected], 1) t.equal(mockAgent[kClients].size, 1) @@ -259,7 +259,7 @@ test('MockAgent - .close should clean up registered clients', async (t) => { // Register a pool const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) t.equal(mockClient[kConnected], 1) t.equal(mockAgent[kClients].size, 1) @@ -996,7 +996,7 @@ test('MockAgent - close removes all registered mock clients', async (t) => { try { await request(`${baseUrl}/foo`, { method: 'GET' }) } catch (err) { - t.type(err, ClientDestroyedError) + t.ok(err instanceof ClientDestroyedError) } }) @@ -1030,7 +1030,7 @@ test('MockAgent - close removes all registered mock pools', async (t) => { try { await request(`${baseUrl}/foo`, { method: 'GET' }) } catch (err) { - t.type(err, ClientDestroyedError) + t.ok(err instanceof ClientDestroyedError) } }) diff --git a/test/mock-client.js b/test/mock-client.js index 7845135b139..9622977cf1e 100644 --- a/test/mock-client.js +++ b/test/mock-client.js @@ -28,7 +28,7 @@ test('MockClient - constructor', t => { t.plan(1) const mockClient = new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) }) - t.type(mockClient, Dispatcher) + t.ok(mockClient instanceof Dispatcher) }) }) @@ -121,7 +121,7 @@ test('MockClient - intercept should return a MockInterceptor', (t) => { method: 'GET' }) - t.type(interceptor, MockInterceptor) + t.ok(interceptor instanceof MockInterceptor) }) test('MockClient - intercept validation', (t) => { @@ -218,7 +218,7 @@ test('MockClient - should be able to set as globalDispatcher', async (t) => { t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) setGlobalDispatcher(mockClient) mockClient.intercept({ @@ -254,7 +254,7 @@ test('MockClient - should support query params', async (t) => { t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) setGlobalDispatcher(mockClient) const query = { @@ -295,7 +295,7 @@ test('MockClient - should intercept query params with hardcoded path', async (t) t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) setGlobalDispatcher(mockClient) const query = { @@ -335,7 +335,7 @@ test('MockClient - should intercept query params regardless of key ordering', as t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) setGlobalDispatcher(mockClient) const query = { @@ -383,7 +383,7 @@ test('MockClient - should be able to use as a local dispatcher', async (t) => { t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) mockClient.intercept({ path: '/foo', @@ -418,7 +418,7 @@ test('MockClient - basic intercept with MockClient.request', async (t) => { const mockAgent = new MockAgent({ connections: 1 }) t.teardown(mockAgent.close.bind(mockAgent)) const mockClient = mockAgent.get(baseUrl) - t.type(mockClient, MockClient) + t.ok(mockClient instanceof MockClient) mockClient.intercept({ path: '/foo?hello=there&see=ya', diff --git a/test/mock-errors.js b/test/mock-errors.js index a96de0bac84..1087f0cf588 100644 --- a/test/mock-errors.js +++ b/test/mock-errors.js @@ -13,7 +13,7 @@ test('mockErrors', (t) => { t.plan(4) const mockError = new mockErrors.MockNotMatchedError() - t.type(mockError, errors.UndiciError) + t.ok(mockError instanceof errors.UndiciError) t.same(mockError.name, 'MockNotMatchedError') t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') t.same(mockError.message, 'The request does not match any registered mock dispatches') @@ -23,7 +23,7 @@ test('mockErrors', (t) => { t.plan(4) const mockError = new mockErrors.MockNotMatchedError('custom message') - t.type(mockError, errors.UndiciError) + t.ok(mockError instanceof errors.UndiciError) t.same(mockError.name, 'MockNotMatchedError') t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') t.same(mockError.message, 'custom message') diff --git a/test/mock-interceptor.js b/test/mock-interceptor.js index a11377d3af0..787878395f4 100644 --- a/test/mock-interceptor.js +++ b/test/mock-interceptor.js @@ -28,7 +28,7 @@ test('MockInterceptor - reply', t => { method: '' }, []) const result = mockInterceptor.reply(200, 'hello') - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) t.test('should error if passed options invalid', t => { @@ -53,7 +53,7 @@ test('MockInterceptor - reply callback', t => { method: '' }, []) const result = mockInterceptor.reply(200, () => 'hello') - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) t.test('should error if passed options invalid', t => { @@ -82,7 +82,7 @@ test('MockInterceptor - reply options callback', t => { statusCode: 200, data: 'hello' })) - t.type(result, MockScope) + t.ok(result instanceof MockScope) // Test parameters @@ -181,7 +181,7 @@ test('MockInterceptor - replyWithError', t => { method: '' }, []) const result = mockInterceptor.replyWithError(new Error('kaboom')) - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) t.test('should error if passed options invalid', t => { @@ -205,7 +205,7 @@ test('MockInterceptor - defaultReplyHeaders', t => { method: '' }, []) const result = mockInterceptor.defaultReplyHeaders({}) - t.type(result, MockInterceptor) + t.ok(result instanceof MockInterceptor) }) t.test('should error if passed options invalid', t => { @@ -229,7 +229,7 @@ test('MockInterceptor - defaultReplyTrailers', t => { method: '' }, []) const result = mockInterceptor.defaultReplyTrailers({}) - t.type(result, MockInterceptor) + t.ok(result instanceof MockInterceptor) }) t.test('should error if passed options invalid', t => { @@ -253,6 +253,6 @@ test('MockInterceptor - replyContentLength', t => { method: '' }, []) const result = mockInterceptor.defaultReplyTrailers({}) - t.type(result, MockInterceptor) + t.ok(result instanceof MockInterceptor) }) }) diff --git a/test/mock-pool.js b/test/mock-pool.js index fa79796471e..ca812d90866 100644 --- a/test/mock-pool.js +++ b/test/mock-pool.js @@ -29,7 +29,7 @@ test('MockPool - constructor', t => { t.plan(1) const mockPool = new MockPool('http://localhost:9999', { agent: new MockAgent() }) - t.type(mockPool, Dispatcher) + t.ok(mockPool instanceof Dispatcher) }) }) @@ -204,7 +204,7 @@ test('MockPool - should be able to set as globalDispatcher', async (t) => { t.teardown(mockAgent.close.bind(mockAgent)) const mockPool = mockAgent.get(baseUrl) - t.type(mockPool, MockPool) + t.ok(mockPool instanceof MockPool) setGlobalDispatcher(mockPool) mockPool.intercept({ @@ -240,7 +240,7 @@ test('MockPool - should be able to use as a local dispatcher', async (t) => { t.teardown(mockAgent.close.bind(mockAgent)) const mockPool = mockAgent.get(baseUrl) - t.type(mockPool, MockPool) + t.ok(mockPool instanceof MockPool) mockPool.intercept({ path: '/foo', @@ -275,7 +275,7 @@ test('MockPool - basic intercept with MockPool.request', async (t) => { const mockAgent = new MockAgent() t.teardown(mockAgent.close.bind(mockAgent)) const mockPool = mockAgent.get(baseUrl) - t.type(mockPool, MockPool) + t.ok(mockPool instanceof MockPool) mockPool.intercept({ path: '/foo?hello=there&see=ya', diff --git a/test/mock-scope.js b/test/mock-scope.js index 605ba582171..d8f41d90198 100644 --- a/test/mock-scope.js +++ b/test/mock-scope.js @@ -14,7 +14,7 @@ test('MockScope - delay', t => { method: '' }, []) const result = mockScope.delay(200) - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) t.test('should error if passed options invalid', t => { @@ -41,7 +41,7 @@ test('MockScope - persist', t => { method: '' }, []) const result = mockScope.persist() - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) }) @@ -55,7 +55,7 @@ test('MockScope - times', t => { method: '' }, []) const result = mockScope.times(200) - t.type(result, MockScope) + t.ok(result instanceof MockScope) }) t.test('should error if passed options invalid', t => { diff --git a/test/parser-issues.js b/test/parser-issues.js index aef7c42664e..3602ae1ba5e 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -56,7 +56,7 @@ test('parser fail', (t) => { path: '/' }, (err, data) => { t.ok(err) - t.type(err, errors.HTTPParserError) + t.ok(err instanceof errors.HTTPParserError) }) }) }) diff --git a/test/pool.js b/test/pool.js index b1c8722913e..af6a019c7a0 100644 --- a/test/pool.js +++ b/test/pool.js @@ -30,7 +30,7 @@ test('throws when connection is infinite', (t) => { try { new Pool(null, { connections: 0 / 0 }) // eslint-disable-line } catch (e) { - t.type(e, errors.InvalidArgumentError) + t.ok(e instanceof errors.InvalidArgumentError) t.equal(e.message, 'invalid connections') } }) @@ -41,7 +41,7 @@ test('throws when connections is negative', (t) => { try { new Pool(null, { connections: -1 }) // eslint-disable-line no-new } catch (e) { - t.type(e, errors.InvalidArgumentError) + t.ok(e instanceof errors.InvalidArgumentError) t.equal(e.message, 'invalid connections') } }) @@ -52,7 +52,7 @@ test('throws when connection is not number', (t) => { try { new Pool(null, { connections: true }) // eslint-disable-line no-new } catch (e) { - t.type(e, errors.InvalidArgumentError) + t.ok(e instanceof errors.InvalidArgumentError) t.equal(e.message, 'invalid connections') } }) @@ -63,7 +63,7 @@ test('throws when factory is not a function', (t) => { try { new Pool(null, { factory: '' }) // eslint-disable-line no-new } catch (e) { - t.type(e, errors.InvalidArgumentError) + t.ok(e instanceof errors.InvalidArgumentError) t.equal(e.message, 'factory must be a function.') } }) @@ -100,7 +100,7 @@ test('connect/disconnect event(s)', (t) => { }) pool.on('disconnect', (origin, [pool, client], error) => { t.ok(client instanceof Client) - t.type(error, errors.InformationalError) + t.ok(error instanceof errors.InformationalError) t.equal(error.code, 'UND_ERR_INFO') t.equal(error.message, 'socket idle timeout') }) @@ -155,7 +155,7 @@ test('basic get', (t) => { client.destroy((err) => { t.error(err) client.close((err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -198,7 +198,7 @@ test('URL as arg', (t) => { client.destroy((err) => { t.error(err) client.close((err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -225,7 +225,7 @@ test('basic get error async/await', (t) => { await client.destroy() await client.close().catch((err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -596,7 +596,7 @@ test('pool pipeline args validation', (t) => { const ret = client.pipeline(null, () => {}) ret.on('error', (err) => { t.ok(/opts/.test(err.message)) - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) @@ -1025,7 +1025,7 @@ test('pool close waits for all requests', (t) => { path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientClosedError) + t.ok(err instanceof errors.ClientClosedError) }) }) }) @@ -1050,7 +1050,7 @@ test('pool destroyed', (t) => { path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) @@ -1095,7 +1095,7 @@ test('pool destroy fails queued requests', (t) => { path: '/', method: 'GET' }, (err) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) }) }) diff --git a/test/request-crlf.js b/test/request-crlf.js index f3572062491..be33f767a4d 100644 --- a/test/request-crlf.js +++ b/test/request-crlf.js @@ -25,7 +25,7 @@ test('should validate content-type CRLF Injection', (t) => { }) t.fail('request should fail') } catch (e) { - t.type(e, errors.InvalidArgumentError) + t.ok(e instanceof errors.InvalidArgumentError) t.equal(e.message, 'invalid content-type header') } }) diff --git a/test/request-timeout.js b/test/request-timeout.js index 49d29a6223c..0db4417e0d5 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -31,7 +31,7 @@ test('request timeout', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) }) }) @@ -53,7 +53,7 @@ test('request timeout with readable body', (t) => { const body = createReadStream(tempfile) client.request({ path: '/', method: 'POST', body }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) }) }) @@ -84,7 +84,7 @@ test('body timeout', (t) => { body.on('data', () => { clock.tick(100) }).on('error', (err) => { - t.type(err, errors.BodyTimeoutError) + t.ok(err instanceof errors.BodyTimeoutError) }) }) @@ -117,7 +117,7 @@ test('overridden request timeout', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', headersTimeout: 50 }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) clock.tick(50) @@ -150,7 +150,7 @@ test('overridden body timeout', (t) => { body.on('data', () => { clock.tick(100) }).on('error', (err) => { - t.type(err, errors.BodyTimeoutError) + t.ok(err instanceof errors.BodyTimeoutError) }) }) @@ -186,7 +186,7 @@ test('With EE signal', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) clock.tick(50) @@ -221,7 +221,7 @@ test('With abort-controller signal', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) clock.tick(50) @@ -257,7 +257,7 @@ test('Abort before timeout (EE)', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) clock.tick(100) }) }) @@ -292,7 +292,7 @@ test('Abort before timeout (abort-controller)', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { - t.type(err, errors.RequestAbortedError) + t.ok(err instanceof errors.RequestAbortedError) clock.tick(100) }) }) @@ -326,15 +326,15 @@ test('Timeout with pipelining', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) }) }) @@ -366,7 +366,7 @@ test('Global option', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) clock.tick(50) @@ -400,7 +400,7 @@ test('Request options overrides global option', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) clock.tick(50) @@ -421,7 +421,7 @@ test('client.destroy should cancel the timeout', (t) => { }) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.ClientDestroyedError) + t.ok(err instanceof errors.ClientDestroyedError) }) client.destroy(err => { @@ -453,7 +453,7 @@ test('client.close should wait for the timeout', (t) => { t.teardown(client.destroy.bind(client)) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) client.close((err) => { @@ -477,7 +477,7 @@ test('Validation', (t) => { }) t.teardown(client.destroy.bind(client)) } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } try { @@ -486,7 +486,7 @@ test('Validation', (t) => { }) t.teardown(client.destroy.bind(client)) } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } try { @@ -495,7 +495,7 @@ test('Validation', (t) => { }) t.teardown(client.destroy.bind(client)) } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } try { @@ -504,7 +504,7 @@ test('Validation', (t) => { }) t.teardown(client.destroy.bind(client)) } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } }) @@ -623,7 +623,7 @@ test('stream timeout', (t) => { }, (result) => { t.fail('Should not be called') }, (err) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) }) }) @@ -661,7 +661,7 @@ test('stream custom timeout', (t) => { }, (result) => { t.fail('Should not be called') }, (err) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) }) }) @@ -715,7 +715,7 @@ test('pipeline timeout', (t) => { } }), (err) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) } ) }) @@ -772,7 +772,7 @@ test('pipeline timeout', (t) => { } }), (err) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) } ) }) @@ -806,7 +806,7 @@ test('client.close should not deadlock', (t) => { path: '/', method: 'GET' }, (err, response) => { - t.type(err, errors.HeadersTimeoutError) + t.ok(err instanceof errors.HeadersTimeoutError) }) client.close((err) => { diff --git a/test/socket-timeout.js b/test/socket-timeout.js index f2ad9faa34a..a0facda8369 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -36,7 +36,7 @@ test('timeout with pipelining 1', (t) => { method: 'GET', opaque: 'asd' }, (err, data) => { - t.type(err, errors.HeadersTimeoutError) // we are expecting an error + t.ok(err instanceof errors.HeadersTimeoutError) // we are expecting an error t.equal(data.opaque, 'asd') }) diff --git a/test/utils/esm-wrapper.mjs b/test/utils/esm-wrapper.mjs index 51f8572a0bf..0e8ac9ffb7a 100644 --- a/test/utils/esm-wrapper.mjs +++ b/test/utils/esm-wrapper.mjs @@ -67,13 +67,13 @@ test('imported errors work with request args validation', (t) => { const client = new Client('http://localhost:5000') client.request(null, (err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) try { client.request(null, 'asd') } catch (err) { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) } }) @@ -83,7 +83,7 @@ test('imported errors work with request args validation promise', (t) => { const client = new Client('http://localhost:5000') client.request(null).catch((err) => { - t.type(err, errors.InvalidArgumentError) + t.ok(err instanceof errors.InvalidArgumentError) }) }) diff --git a/test/wpt/tests/resources/testharness.js b/test/wpt/tests/resources/testharness.js index bc7fb8961b9..a0011f03ac0 100644 --- a/test/wpt/tests/resources/testharness.js +++ b/test/wpt/tests/resources/testharness.js @@ -1247,7 +1247,7 @@ */ function is_node(object) { - // I use duck-typing instead of instanceof, because + // I use duck-typing instead of instanceof because // instanceof doesn't work if the node is from another window (like an // iframe's contentWindow): // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 From 6db4bbe979d6bf1fc41bd106e98dba96e724c421 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 10 Feb 2024 04:22:13 -0500 Subject: [PATCH 013/123] remove useless options in web streams (#2729) --- lib/fetch/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index a989295bc4b..f65bfbe78da 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1118,7 +1118,6 @@ function fetchFinale (fetchParams, response) { controller.enqueue(value) } }, - queuingStrategy: new ByteLengthQueuingStrategy({ highWaterMark: 16384 }), type: 'bytes' }) @@ -1933,7 +1932,6 @@ async function httpNetworkFetch ( // cancelAlgorithm set to cancelAlgorithm. const stream = new ReadableStream( { - highWaterMark: 16384, async start (controller) { fetchParams.controller.controller = controller }, @@ -1943,8 +1941,7 @@ async function httpNetworkFetch ( async cancel (reason) { await cancelAlgorithm(reason) }, - type: 'bytes', - queuingStrategy: new ByteLengthQueuingStrategy({ highWaterMark: 16384 }) + type: 'bytes' } ) From 94a9b5610fc7f791982f0b8039f169e9f67b4e83 Mon Sep 17 00:00:00 2001 From: Sole Cold <53977359+eddienubes@users.noreply.github.com> Date: Sun, 11 Feb 2024 12:31:11 +0100 Subject: [PATCH 014/123] Let's add superagent to the benchmark. closes #2730 (#2731) * feat: added superagent to benchmark.js * feat: added custom http agent to superagent --- benchmarks/benchmark.js | 19 +++++++++++++++++++ package.json | 2 ++ 2 files changed, 21 insertions(+) diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index 32b761a8a56..f63ece5ed16 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -10,6 +10,7 @@ const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') let nodeFetch const axios = require('axios') +let superagent let got const util = require('node:util') @@ -85,6 +86,11 @@ const requestAgent = new http.Agent({ maxSockets: connections }) +const superagentAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + const undiciOptions = { path: '/', method: 'GET', @@ -318,6 +324,16 @@ if (process.env.PORT) { }).catch(console.log) }) } + + experiments.superagent = () => { + return makeParallelRequests(resolve => { + superagent.get(dest.url).pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }) + } } async function main () { @@ -326,6 +342,9 @@ async function main () { nodeFetch = _nodeFetch.default const _got = await import('got') got = _got.default + const _superagent = await import('superagent') + // https://github.com/ladjs/superagent/issues/1540#issue-561464561 + superagent = _superagent.agent().use((req) => req.agent(superagentAgent)) cronometro( experiments, diff --git a/package.json b/package.json index 1ce1a058b1b..7e4f45594d4 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@matteo.collina/tspl": "^0.1.1", "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", + "@types/superagent": "^8.1.3", "abort-controller": "^3.0.0", "axios": "^1.6.5", "borp": "^0.9.1", @@ -128,6 +129,7 @@ "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", + "superagent": "^8.1.2", "tap": "^16.1.0", "tsd": "^0.30.1", "typescript": "^5.0.2", From 1af7a96378ef29d545d573f65564bab0bc60c749 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 11 Feb 2024 12:23:40 -0500 Subject: [PATCH 015/123] convert node build to latin1 (#2673) --- package.json | 2 +- scripts/strip-comments.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 scripts/strip-comments.js diff --git a/package.json b/package.json index 7e4f45594d4..acbb493382d 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "docs" ], "scripts": { - "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names", + "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js", "prebuild:wasm": "node build/wasm.js --prebuild", "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", diff --git a/scripts/strip-comments.js b/scripts/strip-comments.js new file mode 100644 index 00000000000..9e4396a5dea --- /dev/null +++ b/scripts/strip-comments.js @@ -0,0 +1,8 @@ +'use strict' + +const { readFileSync, writeFileSync } = require('node:fs') +const { transcode } = require('node:buffer') + +const buffer = transcode(readFileSync('./undici-fetch.js'), 'utf8', 'latin1') + +writeFileSync('./undici-fetch.js', buffer.toString('latin1')) From 4e69afd820b990dab428543ec39498bca586ffd4 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 12 Feb 2024 05:28:24 -0500 Subject: [PATCH 016/123] simplify formData body parsing (#2735) * simplify formData body parsing * perf: don't copy all headers * fixup --- lib/fetch/body.js | 63 ++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 9ceb094ef8d..2781a7b90d9 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -15,12 +15,12 @@ const { FormData } = require('./formdata') const { kState } = require('./symbols') const { webidl } = require('./webidl') const { Blob, File: NativeFile } = require('node:buffer') -const { kBodyUsed } = require('../core/symbols') const assert = require('node:assert') const { isErrored } = require('../core/util') -const { isUint8Array, isArrayBuffer } = require('util/types') +const { isArrayBuffer } = require('util/types') const { File: UndiciFile } = require('./file') const { serializeAMimeType } = require('./dataURL') +const { Readable } = require('node:stream') /** @type {globalThis['File']} */ const File = NativeFile ?? UndiciFile @@ -291,29 +291,6 @@ function cloneBody (body) { } } -async function * consumeBody (body) { - if (body) { - if (isUint8Array(body)) { - yield body - } else { - const stream = body.stream - - if (util.isDisturbed(stream)) { - throw new TypeError('The body has already been consumed.') - } - - if (stream.locked) { - throw new TypeError('The stream is locked.') - } - - // Compat. - stream[kBodyUsed] = true - - yield * stream - } - } -} - function throwIfAborted (state) { if (state.aborted) { throw new DOMException('The operation was aborted.', 'AbortError') @@ -328,7 +305,7 @@ function bodyMixinMethods (instance) { // given a byte sequence bytes: return a Blob whose // contents are bytes and whose type attribute is this’s // MIME type. - return specConsumeBody(this, (bytes) => { + return consumeBody(this, (bytes) => { let mimeType = bodyMimeType(this) if (mimeType === null) { @@ -348,7 +325,7 @@ function bodyMixinMethods (instance) { // of running consume body with this and the following step // given a byte sequence bytes: return a new ArrayBuffer // whose contents are bytes. - return specConsumeBody(this, (bytes) => { + return consumeBody(this, (bytes) => { return new Uint8Array(bytes).buffer }, instance) }, @@ -356,13 +333,13 @@ function bodyMixinMethods (instance) { text () { // The text() method steps are to return the result of running // consume body with this and UTF-8 decode. - return specConsumeBody(this, utf8DecodeBytes, instance) + return consumeBody(this, utf8DecodeBytes, instance) }, json () { // The json() method steps are to return the result of running // consume body with this and parse JSON from bytes. - return specConsumeBody(this, parseJSONFromBytes, instance) + return consumeBody(this, parseJSONFromBytes, instance) }, async formData () { @@ -375,16 +352,15 @@ function bodyMixinMethods (instance) { // If mimeType’s essence is "multipart/form-data", then: if (mimeType !== null && mimeType.essence === 'multipart/form-data') { - const headers = {} - for (const [key, value] of this.headers) headers[key] = value - const responseFormData = new FormData() let busboy try { busboy = new Busboy({ - headers, + headers: { + 'content-type': serializeAMimeType(mimeType) + }, preservePath: true }) } catch (err) { @@ -427,8 +403,10 @@ function bodyMixinMethods (instance) { busboy.on('error', (err) => reject(new TypeError(err))) }) - if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) - busboy.end() + if (this.body !== null) { + Readable.from(this[kState].body.stream).pipe(busboy) + } + await busboyResolve return responseFormData @@ -442,20 +420,17 @@ function bodyMixinMethods (instance) { // application/x-www-form-urlencoded parser will keep the BOM. // https://url.spec.whatwg.org/#concept-urlencoded-parser // Note that streaming decoder is stateful and cannot be reused - const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + const stream = this[kState].body.stream.pipeThrough(new TextDecoderStream('utf-8', { ignoreBOM: true })) - for await (const chunk of consumeBody(this[kState].body)) { - if (!isUint8Array(chunk)) { - throw new TypeError('Expected Uint8Array chunk') - } - text += streamingDecoder.decode(chunk, { stream: true }) + for await (const chunk of stream) { + text += chunk } - text += streamingDecoder.decode() + entries = new URLSearchParams(text) } catch (err) { // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. // 2. If entries is failure, then throw a TypeError. - throw new TypeError(undefined, { cause: err }) + throw new TypeError(err) } // 3. Return a new FormData object whose entries are entries. @@ -493,7 +468,7 @@ function mixinBody (prototype) { * @param {(value: unknown) => unknown} convertBytesToJSValue * @param {Response|Request} instance */ -async function specConsumeBody (object, convertBytesToJSValue, instance) { +async function consumeBody (object, convertBytesToJSValue, instance) { webidl.brandCheck(object, instance) throwIfAborted(object[kState]) From 42adfca35369c6825ee968c7d7c66da7b55fea81 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:01:36 +0100 Subject: [PATCH 017/123] chore: transform tests from tap to native test runner (#2719) --- test/client-head-reset-override.js | 92 +++++++++++++------------ test/client-post.js | 98 +++++++++++++++------------ test/client-reconnect.js | 57 ++++++++-------- test/client-unref.js | 32 +++++---- test/client-write-max-listeners.js | 31 +++++---- test/connect-errconnect.js | 13 ++-- test/fetch/issue-2294-patch-method.js | 9 +-- test/issue-1670.js | 6 +- test/issue-2065.js | 13 ++-- 9 files changed, 194 insertions(+), 157 deletions(-) diff --git a/test/client-head-reset-override.js b/test/client-head-reset-override.js index 9639e94860d..4582aa0a79c 100644 --- a/test/client-head-reset-override.js +++ b/test/client-head-reset-override.js @@ -1,62 +1,68 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { once } = require('node:events') const { createServer } = require('node:http') -const { test } = require('tap') +const { test, after } = require('node:test') const { Client } = require('..') -test('override HEAD reset', (t) => { +test('override HEAD reset', async (t) => { + t = tspl(t, { plan: 4 }) + const expected = 'testing123' const server = createServer((req, res) => { if (req.method === 'GET') { res.write(expected) } res.end() - }) - t.teardown(server.close.bind(server)) + }).listen(0) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => server.close()) - let done - client.on('disconnect', () => { - if (!done) { - t.fail() - } - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) - client.request({ - path: '/', - method: 'HEAD', - reset: false - }, (err, res) => { - t.error(err) - res.body.resume() - }) + let done + client.on('disconnect', () => { + if (!done) { + t.fail() + } + }) - client.request({ - path: '/', - method: 'HEAD', - reset: false - }, (err, res) => { - t.error(err) - res.body.resume() - }) + client.request({ + path: '/', + method: 'HEAD', + reset: false + }, (err, res) => { + t.ifError(err) + res.body.resume() + }) - client.request({ - path: '/', - method: 'GET', - reset: false - }, (err, res) => { - t.error(err) - let str = '' - res.body.on('data', (data) => { - str += data - }).on('end', () => { - t.same(str, expected) - done = true - t.end() - }) + client.request({ + path: '/', + method: 'HEAD', + reset: false + }, (err, res) => { + t.ifError(err) + res.body.resume() + }) + + client.request({ + path: '/', + method: 'GET', + reset: false + }, (err, res) => { + t.ifError(err) + let str = '' + res.body.on('data', (data) => { + str += data + }).on('end', () => { + t.strictEqual(str, expected) + done = true + t.end() }) }) + + await t.completed }) diff --git a/test/client-post.js b/test/client-post.js index 82203ad36c7..e666faf0714 100644 --- a/test/client-post.js +++ b/test/client-post.js @@ -1,73 +1,83 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Blob } = require('node:buffer') -test('request post blob', { skip: !Blob }, (t) => { - t.plan(4) +test('request post blob', { skip: !Blob }, async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer(async (req, res) => { - t.equal(req.headers['content-type'], 'application/json') + t.strictEqual(req.headers['content-type'], 'application/json') let str = '' for await (const chunk of req) { str += chunk } - t.equal(str, 'asd') + t.strictEqual(str, 'asd') res.end() }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET', - body: new Blob(['asd'], { - type: 'application/json' - }) - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - t.ok(true, 'pass') - }) + after(server.close.bind(server)) + + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`) + after(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET', + body: new Blob(['asd'], { + type: 'application/json' + }) + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + t.end() }) }) + await t.completed }) -test('request post arrayBuffer', { skip: !Blob }, (t) => { - t.plan(3) +test('request post arrayBuffer', { skip: !Blob }, async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer(async (req, res) => { let str = '' for await (const chunk of req) { str += chunk } - t.equal(str, 'asd') + t.strictEqual(str, 'asd') res.end() }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - const buf = Buffer.from('asd') - const dst = new ArrayBuffer(buf.byteLength) - buf.copy(new Uint8Array(dst)) - - client.request({ - path: '/', - method: 'GET', - body: dst - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - t.ok(true, 'pass') - }) + + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + const buf = Buffer.from('asd') + const dst = new ArrayBuffer(buf.byteLength) + buf.copy(new Uint8Array(dst)) + + client.request({ + path: '/', + method: 'GET', + body: dst + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + t.ok(true, 'pass') }) }) + + await t.completed }) diff --git a/test/client-reconnect.js b/test/client-reconnect.js index d8ed2ce4ddd..34355ce40ac 100644 --- a/test/client-reconnect.js +++ b/test/client-reconnect.js @@ -1,54 +1,57 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') const timers = require('../lib/timers') -test('multiple reconnect', (t) => { - t.plan(5) +test('multiple reconnect', async (t) => { + t = tspl(t, { plan: 5 }) let n = 0 const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { n === 0 ? res.destroy() : res.end('ok') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + server.listen(0) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(client.destroy.bind(client)) - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.ok(err) - t.equal(err.code, 'UND_ERR_SOCKET') - }) - - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) - data.body - .resume() - .on('end', () => { - t.ok(true, 'pass') - }) - }) + client.request({ path: '/', method: 'GET' }, (err, data) => { + t.ok(err) + t.strictEqual(err.code, 'UND_ERR_SOCKET') + }) - client.on('disconnect', () => { - if (++n === 1) { + client.request({ path: '/', method: 'GET' }, (err, data) => { + t.ifError(err) + data.body + .resume() + .on('end', () => { t.ok(true, 'pass') - } - process.nextTick(() => { - clock.tick(1000) }) + }) + + client.on('disconnect', () => { + if (++n === 1) { + t.ok(true, 'pass') + } + process.nextTick(() => { + clock.tick(1000) }) }) + await t.completed }) diff --git a/test/client-unref.js b/test/client-unref.js index f46376f9c46..49df4244e8e 100644 --- a/test/client-unref.js +++ b/test/client-unref.js @@ -3,35 +3,41 @@ const { Worker, isMainThread, workerData } = require('node:worker_threads') if (isMainThread) { - const tap = require('tap') + const { tspl } = require('@matteo.collina/tspl') + const { test, after } = require('node:test') + const { once } = require('node:events') const { createServer } = require('node:http') - tap.test('client automatically closes itself when idle', t => { - t.plan(1) + test('client automatically closes itself when idle', async t => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(server.close.bind(server)) server.keepAliveTimeout = 9999 - server.listen(0, () => { - const url = `http://localhost:${server.address().port}` - const worker = new Worker(__filename, { workerData: { url } }) - worker.on('exit', code => { - t.equal(code, 0) - }) + server.listen(0) + + await once(server, 'listening') + const url = `http://localhost:${server.address().port}` + const worker = new Worker(__filename, { workerData: { url } }) + worker.on('exit', code => { + t.strictEqual(code, 0) }) + await t.completed }) - tap.test('client automatically closes itself if the server is not there', t => { - t.plan(1) + test('client automatically closes itself if the server is not there', async t => { + t = tspl(t, { plan: 1 }) const url = 'http://localhost:4242' // hopefully empty port const worker = new Worker(__filename, { workerData: { url } }) worker.on('exit', code => { - t.equal(code, 0) + t.strictEqual(code, 0) }) + + await t.completed }) } else { const { Client } = require('..') diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index 237bb527f10..c76ce60b0ab 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -1,19 +1,21 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -test('socket close listener does not leak', (t) => { - t.plan(32) +test('socket close listener does not leak', async (t) => { + t = tspl(t, { plan: 32 }) const server = createServer() server.on('request', (req, res) => { res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const makeBody = () => { return new Readable({ @@ -26,7 +28,7 @@ test('socket close listener does not leak', (t) => { } const onRequest = (err, data) => { - t.error(err) + t.ifError(err) data.body.on('end', () => t.ok(true, 'pass')).resume() } @@ -36,16 +38,19 @@ test('socket close listener does not leak', (t) => { } } process.on('warning', onWarning) - t.teardown(() => { + after(() => { process.removeListener('warning', onWarning) }) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + server.listen(0) - for (let n = 0; n < 16; ++n) { - client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) - } - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + for (let n = 0; n < 16; ++n) { + client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) + } + + await t.completed }) diff --git a/test/connect-errconnect.js b/test/connect-errconnect.js index 85b11f83847..7c241c1453e 100644 --- a/test/connect-errconnect.js +++ b/test/connect-errconnect.js @@ -1,14 +1,15 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const net = require('node:net') -test('connect-connectionError', t => { - t.plan(2) +test('connect-connectionError', async t => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:9000') - t.teardown(client.close.bind(client)) + after(() => client.close()) client.once('connectionError', () => { t.ok(true, 'pass') @@ -27,6 +28,8 @@ test('connect-connectionError', t => { path: '/', method: 'GET' }, (err) => { - t.equal(err, _err) + t.strictEqual(err, _err) }) + + await t.completed }) diff --git a/test/fetch/issue-2294-patch-method.js b/test/fetch/issue-2294-patch-method.js index bac7ef51aa9..6223989c07c 100644 --- a/test/fetch/issue-2294-patch-method.js +++ b/test/fetch/issue-2294-patch-method.js @@ -1,19 +1,20 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Request } = require('../..') test('Using `patch` method emits a warning.', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const { emitWarning } = process - t.teardown(() => { + after(() => { process.emitWarning = emitWarning }) process.emitWarning = (warning, options) => { - t.equal(options.code, 'UNDICI-FETCH-patch') + t.strictEqual(options.code, 'UNDICI-FETCH-patch') } // eslint-disable-next-line no-new diff --git a/test/issue-1670.js b/test/issue-1670.js index c27bdb272dc..7a0cda32669 100644 --- a/test/issue-1670.js +++ b/test/issue-1670.js @@ -1,12 +1,10 @@ 'use strict' -const { test } = require('tap') +const { test } = require('node:test') const { request } = require('..') -test('https://github.com/mcollina/undici/issues/810', async (t) => { +test('https://github.com/mcollina/undici/issues/810', async () => { const { body } = await request('https://api.github.com/user/emails') await body.text() - - t.end() }) diff --git a/test/issue-2065.js b/test/issue-2065.js index 5080353a584..14bca22d311 100644 --- a/test/issue-2065.js +++ b/test/issue-2065.js @@ -1,18 +1,21 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createServer } = require('node:http') const { once } = require('node:events') const { createReadStream } = require('node:fs') const { File, FormData, request } = require('..') test('undici.request with a FormData body should set content-length header', async (t) => { + t = tspl(t, { plan: 1 }) + const server = createServer((req, res) => { t.ok(req.headers['content-length']) res.end() }).listen(0) - t.teardown(server.close.bind(server)) + after(() => server.close()) await once(server, 'listening') const body = new FormData() @@ -25,12 +28,14 @@ test('undici.request with a FormData body should set content-length header', asy }) test('undici.request with a FormData stream value should set transfer-encoding header', async (t) => { + t = tspl(t, { plan: 1 }) + const server = createServer((req, res) => { - t.equal(req.headers['transfer-encoding'], 'chunked') + t.strictEqual(req.headers['transfer-encoding'], 'chunked') res.end() }).listen(0) - t.teardown(server.close.bind(server)) + after(() => server.close()) await once(server, 'listening') class BlobFromStream { From 7f90f6364900b5b73b56eecec7c7bd2094f4eb34 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:01:44 +0100 Subject: [PATCH 018/123] chore: migrate a batch of tests to node test runner (#2737) --- test/client-connect.js | 46 ++-- test/client-idempotent-body.js | 17 +- test/client-keep-alive.js | 478 +++++++++++++++++---------------- test/dispatcher.js | 6 +- test/errors.js | 32 +-- test/examples.js | 35 ++- test/headers-as-array.js | 94 ++++--- test/headers-crlf.js | 57 ++-- test/http-100.js | 195 +++++++------- test/inflight-and-close.js | 54 ++-- 10 files changed, 547 insertions(+), 467 deletions(-) diff --git a/test/client-connect.js b/test/client-connect.js index 4917a526d0e..e002a42c571 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -1,14 +1,16 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client, errors } = require('..') const http = require('node:http') const EE = require('node:events') const { kBusy } = require('../lib/core/symbols') // TODO: move to test/node-test/client-connect.js -test('connect aborted after connect', (t) => { - t.plan(3) +test('connect aborted after connect', async (t) => { + t = tspl(t, { plan: 3 }) const signal = new EE() const server = http.createServer((req, res) => { @@ -17,22 +19,26 @@ test('connect aborted after connect', (t) => { server.on('connect', (req, c, firstBodyChunk) => { signal.emit('abort') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 3 - }) - t.teardown(client.destroy.bind(client)) - - client.connect({ - path: '/', - signal, - opaque: 'asd' - }, (err, { opaque }) => { - t.equal(opaque, 'asd') - t.ok(err instanceof errors.RequestAbortedError) - }) - t.equal(client[kBusy], true) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 3 }) + after(() => client.close()) + + client.connect({ + path: '/', + signal, + opaque: 'asd' + }, (err, { opaque }) => { + t.strictEqual(opaque, 'asd') + t.ok(err instanceof errors.RequestAbortedError) + }) + t.strictEqual(client[kBusy], true) + + await t.completed }) diff --git a/test/client-idempotent-body.js b/test/client-idempotent-body.js index 0575b6b8ebd..07004cb8296 100644 --- a/test/client-idempotent-body.js +++ b/test/client-idempotent-body.js @@ -1,11 +1,12 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') -test('idempotent retry', (t) => { - t.plan(11) +test('idempotent retry', async (t) => { + t = tspl(t, { plan: 11 }) const body = 'world' const server = createServer((req, res) => { @@ -13,17 +14,17 @@ test('idempotent retry', (t) => { req.on('data', data => { buf += data }).on('end', () => { - t.strictSame(buf, body) + t.strictEqual(buf, body) res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const _err = new Error() @@ -36,8 +37,10 @@ test('idempotent retry', (t) => { }, () => { throw _err }, (err) => { - t.equal(err, _err) + t.strictEqual(err, _err) }) } }) + + await t.completed }) diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index 15e432c7b2c..b521624caec 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -1,6 +1,8 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const timers = require('../lib/timers') const { kConnect } = require('../lib/core/symbols') @@ -8,8 +10,8 @@ const { createServer } = require('node:net') const http = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') -test('keep-alive header', (t) => { - t.plan(2) +test('keep-alive header', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -18,41 +20,41 @@ test('keep-alive header', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 4e3) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 4e3) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() }) + await t.completed }) -test('keep-alive header 0', (t) => { - t.plan(2) +test('keep-alive header 0', async (t) => { + t = tspl(t, { plan: 2 }) const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { - Object.assign(timers, orgTimers) - }) + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -61,31 +63,33 @@ test('keep-alive header 0', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - keepAliveTimeoutThreshold: 500 - }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - client.on('disconnect', () => { - t.ok(true, 'pass') - }) - clock.tick(600) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeoutThreshold: 500 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + client.on('disconnect', () => { + t.ok(true, 'pass') + }) + clock.tick(600) + }).resume() + }) + await t.completed }) -test('keep-alive header 1', (t) => { - t.plan(2) +test('keep-alive header 1', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -94,32 +98,34 @@ test('keep-alive header 1', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 0) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 0) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() }) + await t.completed }) -test('keep-alive header no postfix', (t) => { - t.plan(2) +test('keep-alive header no postfix', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -128,32 +134,34 @@ test('keep-alive header no postfix', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 4e3) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 4e3) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() }) + await t.completed }) -test('keep-alive not timeout', (t) => { - t.plan(2) +test('keep-alive not timeout', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -162,34 +170,36 @@ test('keep-alive not timeout', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - keepAliveTimeout: 1e3 - }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 3e3) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 1e3 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 3e3) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() + }) + await t.completed }) -test('keep-alive threshold', (t) => { - t.plan(2) +test('keep-alive threshold', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -198,35 +208,37 @@ test('keep-alive threshold', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - keepAliveTimeout: 30e3, - keepAliveTimeoutThreshold: 29e3 - }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 5e3) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 30e3, + keepAliveTimeoutThreshold: 29e3 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 5e3) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() + }) + await t.completed }) -test('keep-alive max keepalive', (t) => { - t.plan(2) +test('keep-alive max keepalive', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -235,35 +247,37 @@ test('keep-alive max keepalive', (t) => { socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - keepAliveTimeout: 30e3, - keepAliveMaxTimeout: 1e3 - }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 3e3) - client.on('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + keepAliveTimeout: 30e3, + keepAliveMaxTimeout: 1e3 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 3e3) + client.on('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() + }) + await t.completed }) -test('connection close', (t) => { - t.plan(4) +test('connection close', async (t) => { + t = tspl(t, { plan: 4 }) let close = false const server = createServer((socket) => { @@ -276,84 +290,88 @@ test('connection close', (t) => { socket.write('Connection: close\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 2 - }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client[kConnect](() => { - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 3e3) - client.once('disconnect', () => { - close = false - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 2 + }) + after(() => client.close()) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 3e3) - client.once('disconnect', () => { - t.ok(true, 'pass') - clearTimeout(timeout) - }) - }).resume() - }) + client[kConnect](() => { + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 3e3) + client.once('disconnect', () => { + close = false + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() + }) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + const timeout = setTimeout(() => { + t.fail() + }, 3e3) + client.once('disconnect', () => { + t.ok(true, 'pass') + clearTimeout(timeout) + }) + }).resume() }) }) + await t.completed }) -test('Disable keep alive', (t) => { - t.plan(7) +test('Disable keep alive', async (t) => { + t = tspl(t, { plan: 7 }) const ports = [] const server = http.createServer((req, res) => { - t.notOk(ports.includes(req.socket.remotePort)) + t.strictEqual(ports.includes(req.socket.remotePort), false) ports.push(req.socket.remotePort) - t.match(req.headers, { connection: 'close' }) + t.strictEqual(req.headers.connection, 'close') res.writeHead(200, { connection: 'close' }) res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 }) - t.teardown(client.destroy.bind(client)) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - client.request({ - path: '/', - method: 'GET' - }, (err, { body }) => { - t.error(err) - body.on('end', () => { - t.ok(true, 'pass') - }).resume() - }) - }).resume() - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.on('end', () => { + t.ok(true, 'pass') + }).resume() + }) + }).resume() }) + await t.completed }) diff --git a/test/dispatcher.js b/test/dispatcher.js index 22750a1e81c..d004c5e5e27 100644 --- a/test/dispatcher.js +++ b/test/dispatcher.js @@ -1,14 +1,14 @@ 'use strict' -const t = require('tap') -const { test } = t +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const Dispatcher = require('../lib/dispatcher') class PoorImplementation extends Dispatcher {} test('dispatcher implementation', (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const dispatcher = new Dispatcher() t.throws(() => dispatcher.dispatch(), Error, 'throws on unimplemented dispatch') diff --git a/test/errors.js b/test/errors.js index a6f17ef9b09..e34b406a905 100644 --- a/test/errors.js +++ b/test/errors.js @@ -1,7 +1,7 @@ 'use strict' -const t = require('tap') -const { test } = t +const { tspl } = require('@matteo.collina/tspl') +const { describe, test } = require('node:test') const errors = require('../lib/core/errors') @@ -31,51 +31,47 @@ const scenarios = [ ] scenarios.forEach(scenario => { - test(scenario.name, t => { + describe(scenario.name, () => { const SAMPLE_MESSAGE = 'sample message' const errorWithDefaultMessage = () => new scenario.ErrorClass() const errorWithProvidedMessage = () => new scenario.ErrorClass(SAMPLE_MESSAGE) test('should use default message', t => { - t.plan(1) + t = tspl(t, { plan: 1 }) const error = errorWithDefaultMessage() - t.equal(error.message, scenario.defaultMessage) + t.strictEqual(error.message, scenario.defaultMessage) }) test('should use provided message', t => { - t.plan(1) + t = tspl(t, { plan: 1 }) const error = errorWithProvidedMessage() - t.equal(error.message, SAMPLE_MESSAGE) + t.strictEqual(error.message, SAMPLE_MESSAGE) }) test('should have proper fields', t => { - t.plan(6) + t = tspl(t, { plan: 6 }) const errorInstances = [errorWithDefaultMessage(), errorWithProvidedMessage()] errorInstances.forEach(error => { - t.equal(error.name, scenario.name) - t.equal(error.code, scenario.code) + t.strictEqual(error.name, scenario.name) + t.strictEqual(error.code, scenario.code) t.ok(error.stack) }) }) - - t.end() }) }) -test('Default HTTPParseError Codes', t => { +describe('Default HTTPParseError Codes', () => { test('code and data should be undefined when not set', t => { - t.plan(2) + t = tspl(t, { plan: 2 }) const error = new errors.HTTPParserError('HTTPParserError') - t.equal(error.code, undefined) - t.equal(error.data, undefined) + t.strictEqual(error.code, undefined) + t.strictEqual(error.data, undefined) }) - - t.end() }) diff --git a/test/examples.js b/test/examples.js index 7096970662f..d344236b68d 100644 --- a/test/examples.js +++ b/test/examples.js @@ -1,10 +1,14 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') -const { test } = require('tap') +const { test, after } = require('node:test') +const { once } = require('node:events') const examples = require('../examples/request.js') test('request examples', async (t) => { + t = tspl(t, { plan: 7 }) + let lastReq const exampleServer = createServer((req, res) => { lastReq = req @@ -32,28 +36,33 @@ test('request examples', async (t) => { res.end('{"error":"an error"}') }) - t.teardown(exampleServer.close.bind(exampleServer)) - t.teardown(errorServer.close.bind(errorServer)) + after(() => exampleServer.close()) + after(() => errorServer.close()) + + exampleServer.listen(0) + errorServer.listen(0) - await exampleServer.listen(0) - await errorServer.listen(0) + await Promise.all([ + once(exampleServer, 'listening'), + once(errorServer, 'listening') + ]) await examples.getRequest(exampleServer.address().port) - t.equal(lastReq.method, 'GET') + t.strictEqual(lastReq.method, 'GET') await examples.postJSONRequest(exampleServer.address().port) - t.equal(lastReq.method, 'POST') - t.equal(lastReq.headers['content-type'], 'application/json') + t.strictEqual(lastReq.method, 'POST') + t.strictEqual(lastReq.headers['content-type'], 'application/json') await examples.postFormRequest(exampleServer.address().port) - t.equal(lastReq.method, 'POST') - t.equal(lastReq.headers['content-type'], 'application/x-www-form-urlencoded') + t.strictEqual(lastReq.method, 'POST') + t.strictEqual(lastReq.headers['content-type'], 'application/x-www-form-urlencoded') await examples.deleteRequest(exampleServer.address().port) - t.equal(lastReq.method, 'DELETE') + t.strictEqual(lastReq.method, 'DELETE') await examples.deleteRequest(errorServer.address().port) - t.equal(lastReq.method, 'DELETE') + t.strictEqual(lastReq.method, 'DELETE') - t.end() + await t.completed }) diff --git a/test/headers-as-array.js b/test/headers-as-array.js index e50605332e5..693979e92b7 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -1,102 +1,122 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') -test('handle headers as array', (t) => { - t.plan(1) +test('handle headers as array', async (t) => { + t = tspl(t, { plan: 3 }) const headers = ['a', '1', 'b', '2', 'c', '3'] const server = createServer((req, res) => { - t.match(req.headers, { a: '1', b: '2', c: '3' }) + t.strictEqual(req.headers.a, '1') + t.strictEqual(req.headers.b, '2') + t.strictEqual(req.headers.c, '3') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers - }, () => {}) + }, () => { }) }) + + await t.completed }) -test('handle multi-valued headers as array', (t) => { - t.plan(1) +test('handle multi-valued headers as array', async (t) => { + t = tspl(t, { plan: 4 }) const headers = ['a', '1', 'b', '2', 'c', '3', 'd', '4', 'd', '5'] const server = createServer((req, res) => { - t.match(req.headers, { a: '1', b: '2', c: '3', d: '4, 5' }) + t.strictEqual(req.headers.a, '1') + t.strictEqual(req.headers.b, '2') + t.strictEqual(req.headers.c, '3') + t.strictEqual(req.headers.d, '4, 5') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers - }, () => {}) + }, () => { }) }) + + await t.completed }) -test('handle headers with array', (t) => { - t.plan(1) +test('handle headers with array', async (t) => { + t = tspl(t, { plan: 4 }) const headers = { a: '1', b: '2', c: '3', d: ['4'] } const server = createServer((req, res) => { - t.match(req.headers, { a: '1', b: '2', c: '3', d: '4' }) + t.strictEqual(req.headers.a, '1') + t.strictEqual(req.headers.b, '2') + t.strictEqual(req.headers.c, '3') + t.strictEqual(req.headers.d, '4') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers - }, () => {}) + }, () => { }) }) + + await t.completed }) -test('handle multi-valued headers', (t) => { - t.plan(1) +test('handle multi-valued headers', async (t) => { + t = tspl(t, { plan: 4 }) const headers = { a: '1', b: '2', c: '3', d: ['4', '5'] } const server = createServer((req, res) => { - t.match(req.headers, { a: '1', b: '2', c: '3', d: '4, 5' }) + t.strictEqual(req.headers.a, '1') + t.strictEqual(req.headers.b, '2') + t.strictEqual(req.headers.c, '3') + t.strictEqual(req.headers.d, '4, 5') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers - }, () => {}) + }, () => { }) }) + + await t.completed }) -test('fail if headers array is odd', (t) => { - t.plan(2) +test('fail if headers array is odd', async (t) => { + t = tspl(t, { plan: 2 }) const headers = ['a', '1', 'b', '2', 'c', '3', 'd'] const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -104,20 +124,22 @@ test('fail if headers array is odd', (t) => { headers }, (err) => { t.ok(err instanceof errors.InvalidArgumentError) - t.equal(err.message, 'headers array must be even') + t.strictEqual(err.message, 'headers array must be even') }) }) + + await t.completed }) -test('fail if headers is not an object or an array', (t) => { - t.plan(2) +test('fail if headers is not an object or an array', async (t) => { + t = tspl(t, { plan: 2 }) const headers = 'not an object or an array' const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -125,7 +147,9 @@ test('fail if headers is not an object or an array', (t) => { headers }, (err) => { t.ok(err instanceof errors.InvalidArgumentError) - t.equal(err.message, 'headers must be an object or an array') + t.strictEqual(err.message, 'headers must be an object or an array') }) }) + + await t.completed }) diff --git a/test/headers-crlf.js b/test/headers-crlf.js index d41e924aca1..8660259dbbd 100644 --- a/test/headers-crlf.js +++ b/test/headers-crlf.js @@ -1,36 +1,41 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') -test('CRLF Injection in Nodejs ‘undici’ via host', (t) => { - t.plan(1) +test('CRLF Injection in Nodejs ‘undici’ via host', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer(async (req, res) => { res.end() }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' - - try { - const { body } = await client.request({ - path: '/', - method: 'POST', - headers: { - 'content-type': 'application/json', - host: unsanitizedContentTypeInput - }, - body: 'asd' - }) - await body.dump() - } catch (err) { - t.same(err.code, 'UND_ERR_INVALID_ARG') - } - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' + + try { + const { body } = await client.request({ + path: '/', + method: 'POST', + headers: { + 'content-type': 'application/json', + host: unsanitizedContentTypeInput + }, + body: 'asd' + }) + await body.dump() + } catch (err) { + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + } + await t.completed }) diff --git a/test/http-100.js b/test/http-100.js index 434b53a5c2f..43e75a02e10 100644 --- a/test/http-100.js +++ b/test/http-100.js @@ -1,41 +1,46 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const net = require('node:net') +const { once } = require('node:events') -test('ignore informational response', (t) => { - t.plan(2) +test('ignore informational response', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.writeProcessing() req.pipe(res) }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'POST', - body: 'hello' - }, (err, response) => { - t.error(err) - const bufs = [] - response.body.on('data', (buf) => { - bufs.push(buf) - }) - response.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) - }) + after(() => server.close()) + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'POST', + body: 'hello' + }, (err, response) => { + t.ifError(err) + const bufs = [] + response.body.on('data', (buf) => { + bufs.push(buf) + }) + response.body.on('end', () => { + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) + + await t.completed }) -test('error 103 body', (t) => { - t.plan(2) +test('error 103 body', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((socket) => { socket.write('HTTP/1.1 103 Early Hints\r\n') @@ -43,99 +48,107 @@ test('error 103 body', (t) => { socket.write('\r\n') socket.write('a\r\n') }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.equal(err.code, 'HPE_INVALID_CONSTANT') - }) - client.on('disconnect', () => { - t.ok(true, 'pass') - }) + after(() => server.close()) + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + t.strictEqual(err.code, 'HPE_INVALID_CONSTANT') }) + client.on('disconnect', () => { + t.ok(true, 'pass') + }) + await t.completed }) -test('error 100 body', (t) => { - t.plan(2) +test('error 100 body', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((socket) => { socket.write('HTTP/1.1 100 Early Hints\r\n') socket.write('\r\n') }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.equal(err.message, 'bad response') - }) - client.on('disconnect', () => { - t.ok(true, 'pass') - }) + after(() => server.close()) + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + t.strictEqual(err.message, 'bad response') }) + client.on('disconnect', () => { + t.ok(true, 'pass') + }) + await t.completed }) -test('error 101 upgrade', (t) => { - t.plan(2) +test('error 101 upgrade', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((socket) => { socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: example/1\r\nConnection: Upgrade\r\n') socket.write('\r\n') }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err) => { - t.equal(err.message, 'bad upgrade') - }) - client.on('disconnect', () => { - t.ok(true, 'pass') - }) + after(() => server.close()) + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + t.strictEqual(err.message, 'bad upgrade') }) + client.on('disconnect', () => { + t.ok(true, 'pass') + }) + await t.completed }) -test('1xx response without timeouts', t => { - t.plan(2) +test('1xx response without timeouts', async t => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.writeProcessing() setTimeout(() => req.pipe(res), 2000) }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - bodyTimeout: 0, - headersTimeout: 0 + after(() => server.close()) + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + bodyTimeout: 0, + headersTimeout: 0 + }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'POST', + body: 'hello' + }, (err, response) => { + t.ifError(err) + const bufs = [] + response.body.on('data', (buf) => { + bufs.push(buf) }) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'POST', - body: 'hello' - }, (err, response) => { - t.error(err) - const bufs = [] - response.body.on('data', (buf) => { - bufs.push(buf) - }) - response.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) - }) + response.body.on('end', () => { + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) + await t.completed }) diff --git a/test/inflight-and-close.js b/test/inflight-and-close.js index 576a8c568ff..075cdba1668 100644 --- a/test/inflight-and-close.js +++ b/test/inflight-and-close.js @@ -1,31 +1,37 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { request } = require('..') const http = require('node:http') -const server = http.createServer((req, res) => { - res.writeHead(200) - res.end('Response body') - res.socket.end() // Close the connection immediately with every response -}).listen(0, '127.0.0.1', function () { - const url = `http://127.0.0.1:${this.address().port}` - request(url) - .then(({ statusCode, headers, body }) => { - t.ok(true, 'first response') - body.resume() - body.on('close', function () { - t.ok(true, 'first body closed') - }) - return request(url) - .then(({ statusCode, headers, body }) => { - t.ok(true, 'second response') - body.resume() - body.on('close', function () { - server.close() - }) +test('inflight and close', async (t) => { + t = tspl(t, { plan: 3 }) + + const server = http.createServer((req, res) => { + res.writeHead(200) + res.end('Response body') + res.socket.end() // Close the connection immediately with every response + }).listen(0, '127.0.0.1', function () { + const url = `http://127.0.0.1:${this.address().port}` + request(url) + .then(({ statusCode, headers, body }) => { + t.ok(true, 'first response') + body.resume() + body.on('close', function () { + t.ok(true, 'first body closed') }) - }).catch((err) => { - t.error(err) - }) + return request(url) + .then(({ statusCode, headers, body }) => { + t.ok(true, 'second response') + body.resume() + body.on('close', function () { + server.close() + }) + }) + }).catch((err) => { + t.ifError(err) + }) + }) + await t.completed }) From c27b00e5aca8ccc98f24e1e08ce2b7f10b46101f Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:01:52 +0100 Subject: [PATCH 019/123] chore: migrate a batch of tests to node test runner (#2739) --- test/mock-agent.js | 775 +++++++++++---------- test/mock-client.js | 140 ++-- test/mock-errors.js | 41 +- test/mock-interceptor-unused-assertions.js | 104 +-- test/mock-interceptor.js | 126 ++-- test/mock-pool.js | 136 ++-- test/mock-scope.js | 35 +- test/mock-utils.js | 63 +- 8 files changed, 703 insertions(+), 717 deletions(-) diff --git a/test/mock-agent.js b/test/mock-agent.js index ed58464b527..80b04e658e8 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { createServer } = require('node:http') const { promisify } = require('node:util') const { request, setGlobalDispatcher, MockAgent, Agent } = require('..') @@ -14,93 +15,87 @@ const Dispatcher = require('../lib/dispatcher') const { MockNotMatchedError } = require('../lib/mock/mock-errors') const { fetch } = require('..') -test('MockAgent - constructor', t => { - t.plan(5) - - t.test('sets up mock agent', t => { - t.plan(1) +describe('MockAgent - constructor', () => { + test('sets up mock agent', t => { + t = tspl(t, { plan: 1 }) t.doesNotThrow(() => new MockAgent()) }) - t.test('should implement the Dispatcher API', t => { - t.plan(1) + test('should implement the Dispatcher API', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() t.ok(mockAgent instanceof Dispatcher) }) - t.test('sets up mock agent with single connection', t => { - t.plan(1) + test('sets up mock agent with single connection', t => { + t = tspl(t, { plan: 1 }) t.doesNotThrow(() => new MockAgent({ connections: 1 })) }) - t.test('should error passed agent is not valid', t => { - t.plan(2) + test('should error passed agent is not valid', t => { + t = tspl(t, { plan: 2 }) t.throws(() => new MockAgent({ agent: {} }), new InvalidArgumentError('Argument opts.agent must implement Agent')) t.throws(() => new MockAgent({ agent: { dispatch: '' } }), new InvalidArgumentError('Argument opts.agent must implement Agent')) }) - t.test('should be able to specify the agent to mock', t => { - t.plan(1) + test('should be able to specify the agent to mock', t => { + t = tspl(t, { plan: 1 }) const agent = new Agent() - t.teardown(agent.close.bind(agent)) + after(() => agent.close()) const mockAgent = new MockAgent({ agent }) - t.equal(mockAgent[kAgent], agent) + t.strictEqual(mockAgent[kAgent], agent) }) }) -test('MockAgent - get', t => { - t.plan(3) - - t.test('should return MockClient', (t) => { - t.plan(1) +describe('MockAgent - get', t => { + test('should return MockClient', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) }) - t.test('should return MockPool', (t) => { - t.plan(1) + test('should return MockPool', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) t.ok(mockPool instanceof MockPool) }) - t.test('should return the same instance if already created', (t) => { - t.plan(1) + test('should return the same instance if already created', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool1 = mockAgent.get(baseUrl) const mockPool2 = mockAgent.get(baseUrl) - t.equal(mockPool1, mockPool2) + t.strictEqual(mockPool1, mockPool2) }) }) -test('MockAgent - dispatch', t => { - t.plan(3) - - t.test('should call the dispatch method of the MockPool', (t) => { - t.plan(1) +describe('MockAgent - dispatch', () => { + test('should call the dispatch method of the MockPool', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -121,13 +116,13 @@ test('MockAgent - dispatch', t => { })) }) - t.test('should call the dispatch method of the MockClient', (t) => { - t.plan(1) + test('should call the dispatch method of the MockClient', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) @@ -148,13 +143,13 @@ test('MockAgent - dispatch', t => { })) }) - t.test('should throw if handler is not valid on redirect', (t) => { - t.plan(7) + test('should throw if handler is not valid on redirect', (t) => { + t = tspl(t, { plan: 7 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) t.throws(() => mockAgent.dispatch({ origin: baseUrl, @@ -233,7 +228,7 @@ test('MockAgent - dispatch', t => { }) test('MockAgent - .close should clean up registered pools', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const baseUrl = 'http://localhost:9999' @@ -243,15 +238,15 @@ test('MockAgent - .close should clean up registered pools', async (t) => { const mockPool = mockAgent.get(baseUrl) t.ok(mockPool instanceof MockPool) - t.equal(mockPool[kConnected], 1) - t.equal(mockAgent[kClients].size, 1) + t.strictEqual(mockPool[kConnected], 1) + t.strictEqual(mockAgent[kClients].size, 1) await mockAgent.close() - t.equal(mockPool[kConnected], 0) - t.equal(mockAgent[kClients].size, 0) + t.strictEqual(mockPool[kConnected], 0) + t.strictEqual(mockAgent[kClients].size, 0) }) test('MockAgent - .close should clean up registered clients', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const baseUrl = 'http://localhost:9999' @@ -261,15 +256,15 @@ test('MockAgent - .close should clean up registered clients', async (t) => { const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) - t.equal(mockClient[kConnected], 1) - t.equal(mockAgent[kClients].size, 1) + t.strictEqual(mockClient[kConnected], 1) + t.strictEqual(mockAgent[kClients].size, 1) await mockAgent.close() - t.equal(mockClient[kConnected], 0) - t.equal(mockAgent[kClients].size, 0) + t.strictEqual(mockClient[kConnected], 0) + t.strictEqual(mockAgent[kClients].size, 0) }) test('MockAgent - [kClients] should match encapsulated agent', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -277,14 +272,14 @@ test('MockAgent - [kClients] should match encapsulated agent', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const agent = new Agent() - t.teardown(agent.close.bind(agent)) + after(() => agent.close()) const mockAgent = new MockAgent({ agent }) @@ -295,11 +290,11 @@ test('MockAgent - [kClients] should match encapsulated agent', async (t) => { }).reply(200, 'hello') // The MockAgent should encapsulate the input agent clients - t.equal(mockAgent[kClients].size, agent[kClients].size) + t.strictEqual(mockAgent[kClients].size, agent[kClients].size) }) test('MockAgent - basic intercept with MockAgent.request', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -307,14 +302,14 @@ test('MockAgent - basic intercept with MockAgent.request', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -332,18 +327,18 @@ test('MockAgent - basic intercept with MockAgent.request', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - basic intercept with request', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -351,7 +346,7 @@ test('MockAgent - basic intercept with request', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -359,7 +354,7 @@ test('MockAgent - basic intercept with request', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -375,18 +370,18 @@ test('MockAgent - basic intercept with request', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - should support local agents', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -394,7 +389,7 @@ test('MockAgent - should support local agents', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -402,7 +397,7 @@ test('MockAgent - should support local agents', async (t) => { const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -421,18 +416,18 @@ test('MockAgent - should support local agents', async (t) => { body: 'form1=data1&form2=data2', dispatcher: mockAgent }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - should support specifying custom agents to mock', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -440,14 +435,14 @@ test('MockAgent - should support specifying custom agents to mock', async (t) => t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const agent = new Agent() - t.teardown(agent.close.bind(agent)) + after(() => agent.close()) const mockAgent = new MockAgent({ agent }) setGlobalDispatcher(mockAgent) @@ -468,18 +463,18 @@ test('MockAgent - should support specifying custom agents to mock', async (t) => method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - basic Client intercept with request', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -487,7 +482,7 @@ test('MockAgent - basic Client intercept with request', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -495,7 +490,7 @@ test('MockAgent - basic Client intercept with request', async (t) => { const mockAgent = new MockAgent({ connections: 1 }) setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) mockClient.intercept({ @@ -513,18 +508,18 @@ test('MockAgent - basic Client intercept with request', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - basic intercept with multiple pools', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -532,7 +527,7 @@ test('MockAgent - basic intercept with multiple pools', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -540,7 +535,7 @@ test('MockAgent - basic intercept with multiple pools', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool1 = mockAgent.get(baseUrl) const mockPool2 = mockAgent.get('http://localhost:9999') @@ -565,18 +560,18 @@ test('MockAgent - basic intercept with multiple pools', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar-1' }) }) test('MockAgent - should handle multiple responses for an interceptor', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -584,7 +579,7 @@ test('MockAgent - should handle multiple responses for an interceptor', async (t t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -592,7 +587,7 @@ test('MockAgent - should handle multiple responses for an interceptor', async (t const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -615,11 +610,11 @@ test('MockAgent - should handle multiple responses for an interceptor', async (t const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) } @@ -628,26 +623,26 @@ test('MockAgent - should handle multiple responses for an interceptor', async (t const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { hello: 'there' }) } }) test('MockAgent - should call original Pool dispatch if request not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -655,28 +650,28 @@ test('MockAgent - should call original Pool dispatch if request not found', asyn const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - should call original Client dispatch if request not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -684,20 +679,20 @@ test('MockAgent - should call original Client dispatch if request not found', as const mockAgent = new MockAgent({ connections: 1 }) setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - should handle string responses', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -705,7 +700,7 @@ test('MockAgent - should handle string responses', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -713,7 +708,7 @@ test('MockAgent - should handle string responses', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -724,22 +719,20 @@ test('MockAgent - should handle string responses', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, async (t) => { - t.plan(5) - const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) await Promise.all([...Array(5).keys()].map(idx => - t.test(`concurrent job (${idx})`, async (innerTest) => { - innerTest.plan(2) + test(`concurrent job (${idx})`, async (t) => { + t = tspl(t, { plan: 2 }) const baseUrl = 'http://localhost:9999' @@ -752,10 +745,10 @@ test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, as const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - innerTest.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const jsonResponse = JSON.parse(await getResponse(body)) - innerTest.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: `bar ${idx}` }) }) @@ -763,7 +756,7 @@ test('MockAgent - should handle basic concurrency for requests', { jobs: 5 }, as }) test('MockAgent - handle delays to simulate work', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -771,7 +764,7 @@ test('MockAgent - handle delays to simulate work', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -779,7 +772,7 @@ test('MockAgent - handle delays to simulate work', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -792,16 +785,16 @@ test('MockAgent - handle delays to simulate work', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') const elapsedInMs = process.hrtime(start)[1] / 1e6 t.ok(elapsedInMs >= 50, `Elapsed time is not greater than 50ms: ${elapsedInMs}`) }) test('MockAgent - should persist requests', async (t) => { - t.plan(8) + t = tspl(t, { plan: 8 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -809,7 +802,7 @@ test('MockAgent - should persist requests', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -817,7 +810,7 @@ test('MockAgent - should persist requests', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -836,12 +829,12 @@ test('MockAgent - should persist requests', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) } @@ -851,19 +844,19 @@ test('MockAgent - should persist requests', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) } }) test('MockAgent - handle persists with delayed requests', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -871,7 +864,7 @@ test('MockAgent - handle persists with delayed requests', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -879,7 +872,7 @@ test('MockAgent - handle persists with delayed requests', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -891,25 +884,25 @@ test('MockAgent - handle persists with delayed requests', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') } { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'POST' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') } }) test('MockAgent - calling close on a mock pool should not affect other mock pools', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -917,7 +910,7 @@ test('MockAgent - calling close on a mock pool should not affect other mock pool t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -925,7 +918,7 @@ test('MockAgent - calling close on a mock pool should not affect other mock pool const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPoolToClose = mockAgent.get('http://localhost:9999') mockPoolToClose.intercept({ @@ -949,25 +942,25 @@ test('MockAgent - calling close on a mock pool should not affect other mock pool const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, body } = await request(`${baseUrl}/bar`, { method: 'POST' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'bar') + t.strictEqual(response, 'bar') } }) test('MockAgent - close removes all registered mock clients', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -975,7 +968,7 @@ test('MockAgent - close removes all registered mock clients', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -991,7 +984,7 @@ test('MockAgent - close removes all registered mock clients', async (t) => { }).reply(200, 'foo') await mockAgent.close() - t.equal(mockAgent[kClients].size, 0) + t.strictEqual(mockAgent[kClients].size, 0) try { await request(`${baseUrl}/foo`, { method: 'GET' }) @@ -1001,7 +994,7 @@ test('MockAgent - close removes all registered mock clients', async (t) => { }) test('MockAgent - close removes all registered mock pools', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1009,7 +1002,7 @@ test('MockAgent - close removes all registered mock pools', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1025,7 +1018,7 @@ test('MockAgent - close removes all registered mock pools', async (t) => { }).reply(200, 'foo') await mockAgent.close() - t.equal(mockAgent[kClients].size, 0) + t.strictEqual(mockAgent[kClients].size, 0) try { await request(`${baseUrl}/foo`, { method: 'GET' }) @@ -1035,7 +1028,7 @@ test('MockAgent - close removes all registered mock pools', async (t) => { }) test('MockAgent - should handle replyWithError', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1043,7 +1036,7 @@ test('MockAgent - should handle replyWithError', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1051,7 +1044,7 @@ test('MockAgent - should handle replyWithError', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1063,15 +1056,15 @@ test('MockAgent - should handle replyWithError', async (t) => { }) test('MockAgent - should support setting a reply to respond a set amount of times', async (t) => { - t.plan(9) + t = tspl(t, { plan: 9 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1079,7 +1072,7 @@ test('MockAgent - should support setting a reply to respond a set amount of time const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1089,32 +1082,32 @@ test('MockAgent - should support setting a reply to respond a set amount of time { const { statusCode, body } = await request(`${baseUrl}/foo`) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, body } = await request(`${baseUrl}/foo`) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, headers, body } = await request(`${baseUrl}/foo`) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') } }) test('MockAgent - persist overrides times', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1122,7 +1115,7 @@ test('MockAgent - persist overrides times', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1130,7 +1123,7 @@ test('MockAgent - persist overrides times', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1142,42 +1135,42 @@ test('MockAgent - persist overrides times', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } }) test('MockAgent - matcher should not find mock dispatch if path is of unsupported type', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1185,7 +1178,7 @@ test('MockAgent - matcher should not find mock dispatch if path is of unsupporte const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1196,14 +1189,14 @@ test('MockAgent - matcher should not find mock dispatch if path is of unsupporte const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - should match path with regex', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1211,7 +1204,7 @@ test('MockAgent - should match path with regex', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1219,7 +1212,7 @@ test('MockAgent - should match path with regex', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1231,25 +1224,25 @@ test('MockAgent - should match path with regex', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } { const { statusCode, body } = await request(`${baseUrl}/hello/foobar`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } }) test('MockAgent - should match path with function', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1257,7 +1250,7 @@ test('MockAgent - should match path with function', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1265,7 +1258,7 @@ test('MockAgent - should match path with function', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1276,14 +1269,14 @@ test('MockAgent - should match path with function', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match method with regex', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1291,7 +1284,7 @@ test('MockAgent - should match method with regex', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1299,7 +1292,7 @@ test('MockAgent - should match method with regex', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1310,14 +1303,14 @@ test('MockAgent - should match method with regex', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match method with function', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1325,7 +1318,7 @@ test('MockAgent - should match method with function', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1333,7 +1326,7 @@ test('MockAgent - should match method with function', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1344,14 +1337,14 @@ test('MockAgent - should match method with function', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match body with regex', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1359,7 +1352,7 @@ test('MockAgent - should match body with regex', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1367,7 +1360,7 @@ test('MockAgent - should match body with regex', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1380,14 +1373,14 @@ test('MockAgent - should match body with regex', async (t) => { method: 'GET', body: 'hello=there' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match body with function', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1395,7 +1388,7 @@ test('MockAgent - should match body with function', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1403,7 +1396,7 @@ test('MockAgent - should match body with function', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1416,21 +1409,21 @@ test('MockAgent - should match body with function', async (t) => { method: 'GET', body: 'hello=there' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match headers with string', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.end('should not be called') t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1438,7 +1431,7 @@ test('MockAgent - should match headers with string', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1489,21 +1482,21 @@ test('MockAgent - should match headers with string', async (t) => { Host: 'example.com' } }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match headers with regex', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.end('should not be called') t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1511,7 +1504,7 @@ test('MockAgent - should match headers with regex', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1562,21 +1555,21 @@ test('MockAgent - should match headers with regex', async (t) => { Host: 'example.com' } }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match headers with function', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.end('should not be called') t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1584,7 +1577,7 @@ test('MockAgent - should match headers with function', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1635,14 +1628,14 @@ test('MockAgent - should match headers with function', async (t) => { Host: 'example.com' } }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match url with regex', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1650,7 +1643,7 @@ test('MockAgent - should match url with regex', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1658,7 +1651,7 @@ test('MockAgent - should match url with regex', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(new RegExp(baseUrl)) mockPool.intercept({ @@ -1669,14 +1662,14 @@ test('MockAgent - should match url with regex', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - should match url with function', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1684,7 +1677,7 @@ test('MockAgent - should match url with function', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1692,7 +1685,7 @@ test('MockAgent - should match url with function', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get((value) => baseUrl === value) mockPool.intercept({ @@ -1703,14 +1696,14 @@ test('MockAgent - should match url with function', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - handle default reply headers', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1718,7 +1711,7 @@ test('MockAgent - handle default reply headers', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1726,7 +1719,7 @@ test('MockAgent - handle default reply headers', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1737,18 +1730,18 @@ test('MockAgent - handle default reply headers', async (t) => { const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.same(headers, { + t.strictEqual(statusCode, 200) + t.deepStrictEqual(headers, { foo: 'bar', hello: 'there' }) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - handle default reply trailers', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1756,7 +1749,7 @@ test('MockAgent - handle default reply trailers', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1764,7 +1757,7 @@ test('MockAgent - handle default reply trailers', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1775,18 +1768,18 @@ test('MockAgent - handle default reply trailers', async (t) => { const { statusCode, trailers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.same(trailers, { + t.strictEqual(statusCode, 200) + t.deepStrictEqual(trailers, { foo: 'bar', hello: 'there' }) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - return calculated content-length if specified', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1794,7 +1787,7 @@ test('MockAgent - return calculated content-length if specified', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1802,7 +1795,7 @@ test('MockAgent - return calculated content-length if specified', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1813,18 +1806,18 @@ test('MockAgent - return calculated content-length if specified', async (t) => { const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.same(headers, { + t.strictEqual(statusCode, 200) + t.deepStrictEqual(headers, { hello: 'there', - 'content-length': 3 + 'content-length': '3' }) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') }) test('MockAgent - return calculated content-length for object response if specified', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -1832,7 +1825,7 @@ test('MockAgent - return calculated content-length for object response if specif t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1840,7 +1833,7 @@ test('MockAgent - return calculated content-length for object response if specif const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1851,26 +1844,26 @@ test('MockAgent - return calculated content-length for object response if specif const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.same(headers, { + t.strictEqual(statusCode, 200) + t.deepStrictEqual(headers, { hello: 'there', - 'content-length': 13 + 'content-length': '13' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { foo: 'bar' }) + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) test('MockAgent - should activate and deactivate mock clients', async (t) => { - t.plan(9) + t = tspl(t, { plan: 9 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1878,7 +1871,7 @@ test('MockAgent - should activate and deactivate mock clients', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1890,10 +1883,10 @@ test('MockAgent - should activate and deactivate mock clients', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } mockAgent.deactivate() @@ -1902,11 +1895,11 @@ test('MockAgent - should activate and deactivate mock clients', async (t) => { const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') } mockAgent.activate() @@ -1915,23 +1908,23 @@ test('MockAgent - should activate and deactivate mock clients', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.equal(response, 'foo') + t.strictEqual(response, 'foo') } }) test('MockAgent - enableNetConnect should allow all original dispatches to be called if dispatch not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1939,7 +1932,7 @@ test('MockAgent - enableNetConnect should allow all original dispatches to be ca const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1952,23 +1945,23 @@ test('MockAgent - enableNetConnect should allow all original dispatches to be ca const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - enableNetConnect with a host string should allow all original dispatches to be called if mockDispatch not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -1976,7 +1969,7 @@ test('MockAgent - enableNetConnect with a host string should allow all original const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -1989,23 +1982,23 @@ test('MockAgent - enableNetConnect with a host string should allow all original const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - enableNetConnect when called with host string multiple times should allow all original dispatches to be called if mockDispatch not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2013,7 +2006,7 @@ test('MockAgent - enableNetConnect when called with host string multiple times s const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2027,23 +2020,23 @@ test('MockAgent - enableNetConnect when called with host string multiple times s const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - enableNetConnect with a host regex should allow all original dispatches to be called if mockDispatch not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2051,7 +2044,7 @@ test('MockAgent - enableNetConnect with a host regex should allow all original d const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2064,23 +2057,23 @@ test('MockAgent - enableNetConnect with a host regex should allow all original d const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - enableNetConnect with a function should allow all original dispatches to be called if mockDispatch not found', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2088,7 +2081,7 @@ test('MockAgent - enableNetConnect with a function should allow all original dis const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2101,19 +2094,19 @@ test('MockAgent - enableNetConnect with a function should allow all original dis const { statusCode, headers, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const response = await getResponse(body) - t.equal(response, 'hello') + t.strictEqual(response, 'hello') }) test('MockAgent - enableNetConnect with an unknown input should throw', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:9999') mockPool.intercept({ @@ -2125,14 +2118,14 @@ test('MockAgent - enableNetConnect with an unknown input should throw', async (t }) test('MockAgent - enableNetConnect should throw if dispatch not matched for path and the origin was not allowed by net connect', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2140,7 +2133,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for path const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2156,14 +2149,14 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for path }) test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2171,7 +2164,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for meth const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2187,14 +2180,14 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for meth }) test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2202,7 +2195,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for body const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2220,14 +2213,14 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for body }) test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2235,7 +2228,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for head const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2257,15 +2250,15 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for head }) test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { - t.equal(req.url, '/foo') - t.equal(req.method, 'GET') + t.strictEqual(req.url, '/foo') + t.strictEqual(req.method, 'GET') res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2273,7 +2266,7 @@ test('MockAgent - disableNetConnect should throw if dispatch not found by net co const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ @@ -2289,14 +2282,14 @@ test('MockAgent - disableNetConnect should throw if dispatch not found by net co }) test('MockAgent - headers function interceptor', async (t) => { - t.plan(7) + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2304,7 +2297,7 @@ test('MockAgent - headers function interceptor', async (t) => { const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) // Disable net connect so we can make sure it matches properly @@ -2314,7 +2307,7 @@ test('MockAgent - headers function interceptor', async (t) => { path: '/foo', method: 'GET', headers (headers) { - t.equal(typeof headers, 'object') + t.strictEqual(typeof headers, 'object') return !Object.keys(headers).includes('authorization') } }).reply(200, 'foo').times(2) @@ -2333,27 +2326,27 @@ test('MockAgent - headers function interceptor', async (t) => { foo: 'bar' } }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) } { const { statusCode } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) } }) test('MockAgent - clients are not garbage collected', async (t) => { const samples = 250 - t.plan(2) + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { t.fail('should not be called') t.end() res.end('should not be called') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2394,16 +2387,18 @@ test('MockAgent - clients are not garbage collected', async (t) => { results.add(statusCode) } - t.equal(results.size, 1) + t.strictEqual(results.size, 1) t.ok(results.has(200)) }) // https://github.com/nodejs/undici/issues/1321 test('MockAgent - using fetch yields correct statusText', async (t) => { + t = tspl(t, { plan: 4 }) + const mockAgent = new MockAgent() mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:3000') @@ -2414,8 +2409,8 @@ test('MockAgent - using fetch yields correct statusText', async (t) => { const { status, statusText } = await fetch('http://localhost:3000/statusText') - t.equal(status, 200) - t.equal(statusText, 'OK') + t.strictEqual(status, 200) + t.strictEqual(statusText, 'OK') mockPool.intercept({ path: '/unknownStatusText', @@ -2423,17 +2418,19 @@ test('MockAgent - using fetch yields correct statusText', async (t) => { }).reply(420, 'Everyday') const unknownStatusCodeRes = await fetch('http://localhost:3000/unknownStatusText') - t.equal(unknownStatusCodeRes.status, 420) - t.equal(unknownStatusCodeRes.statusText, 'unknown') + t.strictEqual(unknownStatusCodeRes.status, 420) + t.strictEqual(unknownStatusCodeRes.statusText, 'unknown') t.end() }) // https://github.com/nodejs/undici/issues/1556 test('MockAgent - using fetch yields a headers object in the reply callback', async (t) => { + t = tspl(t, { plan: 1 }) + const mockAgent = new MockAgent() mockAgent.disableNetConnect() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:3000') @@ -2441,7 +2438,7 @@ test('MockAgent - using fetch yields a headers object in the reply callback', as path: '/headers', method: 'GET' }).reply(200, (opts) => { - t.same(opts.headers, { + t.deepStrictEqual(opts.headers, { accept: '*/*', 'accept-language': '*', 'sec-fetch-mode': 'cors', @@ -2456,15 +2453,17 @@ test('MockAgent - using fetch yields a headers object in the reply callback', as dispatcher: mockAgent }) - t.end() + await t.completed }) // https://github.com/nodejs/undici/issues/1579 test('MockAgent - headers in mock dispatcher intercept should be case-insensitive', async (t) => { + t = tspl(t, { plan: 1 }) + const mockAgent = new MockAgent() mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('https://example.com') @@ -2485,11 +2484,15 @@ test('MockAgent - headers in mock dispatcher intercept should be case-insensitiv } }) - t.end() + t.ok(true, 'end') + + await t.completed }) // https://github.com/nodejs/undici/issues/1757 test('MockAgent - reply callback can be asynchronous', async (t) => { + t = tspl(t, { plan: 2 }) + class MiniflareDispatcher extends Dispatcher { constructor (inner, options) { super(options) @@ -2514,7 +2517,7 @@ test('MockAgent - reply callback can be asynchronous', async (t) => { mockAgent.disableNetConnect() setGlobalDispatcher(new MiniflareDispatcher(mockAgent)) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) mockClient.intercept({ path: () => true, @@ -2538,7 +2541,7 @@ test('MockAgent - reply callback can be asynchronous', async (t) => { body: JSON.stringify({ foo: 'bar' }) }) - t.same(await response.json(), { foo: 'bar' }) + t.deepStrictEqual(await response.json(), { foo: 'bar' }) } { @@ -2557,11 +2560,13 @@ test('MockAgent - reply callback can be asynchronous', async (t) => { duplex: 'half' }) - t.same(await response.json(), { foo: 'bar' }) + t.deepStrictEqual(await response.json(), { foo: 'bar' }) } }) test('MockAgent - headers should be array of strings', async (t) => { + t = tspl(t, { plan: 1 }) + const mockAgent = new MockAgent() mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) @@ -2585,7 +2590,7 @@ test('MockAgent - headers should be array of strings', async (t) => { method: 'GET' }) - t.same(headers['set-cookie'], [ + t.deepStrictEqual(headers['set-cookie'], [ 'foo=bar', 'bar=baz', 'baz=qux' @@ -2594,7 +2599,7 @@ test('MockAgent - headers should be array of strings', async (t) => { // https://github.com/nodejs/undici/issues/2418 test('MockAgent - Sending ReadableStream body', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) @@ -2604,8 +2609,8 @@ test('MockAgent - Sending ReadableStream body', async (t) => { req.pipe(res) }) - t.teardown(mockAgent.close.bind(mockAgent)) - t.teardown(server.close.bind(server)) + after(() => mockAgent.close()) + after(() => server.close()) await promisify(server.listen.bind(server))(0) @@ -2622,18 +2627,18 @@ test('MockAgent - Sending ReadableStream body', async (t) => { duplex: 'half' }) - t.same(await response.text(), 'test') + t.deepStrictEqual(await response.text(), 'test') }) // https://github.com/nodejs/undici/issues/2616 test('MockAgent - headers should be array of strings (fetch)', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:3000') @@ -2652,5 +2657,5 @@ test('MockAgent - headers should be array of strings (fetch)', async (t) => { method: 'GET' }) - t.same(response.headers.getSetCookie(), ['foo=bar', 'bar=baz', 'baz=qux']) + t.deepStrictEqual(response.headers.getSetCookie(), ['foo=bar', 'bar=baz', 'baz=qux']) }) diff --git a/test/mock-client.js b/test/mock-client.js index 9622977cf1e..ef6721b42be 100644 --- a/test/mock-client.js +++ b/test/mock-client.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { createServer } = require('node:http') const { promisify } = require('node:util') const { MockAgent, MockClient, setGlobalDispatcher, request } = require('..') @@ -11,37 +12,33 @@ const { MockInterceptor } = require('../lib/mock/mock-interceptor') const { getResponse } = require('../lib/mock/mock-utils') const Dispatcher = require('../lib/dispatcher') -test('MockClient - constructor', t => { - t.plan(3) - - t.test('fails if opts.agent does not implement `get` method', t => { - t.plan(1) +describe('MockClient - constructor', () => { + test('fails if opts.agent does not implement `get` method', t => { + t = tspl(t, { plan: 1 }) t.throws(() => new MockClient('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError) }) - t.test('sets agent', t => { - t.plan(1) + test('sets agent', t => { + t = tspl(t, { plan: 1 }) t.doesNotThrow(() => new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) })) }) - t.test('should implement the Dispatcher API', t => { - t.plan(1) + test('should implement the Dispatcher API', t => { + t = tspl(t, { plan: 1 }) const mockClient = new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) }) t.ok(mockClient instanceof Dispatcher) }) }) -test('MockClient - dispatch', t => { - t.plan(2) - - t.test('should handle a single interceptor', (t) => { - t.plan(1) +describe('MockClient - dispatch', () => { + test('should handle a single interceptor', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) @@ -70,13 +67,13 @@ test('MockClient - dispatch', t => { })) }) - t.test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => { - t.plan(1) + test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) @@ -107,12 +104,12 @@ test('MockClient - dispatch', t => { }) test('MockClient - intercept should return a MockInterceptor', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) @@ -124,62 +121,60 @@ test('MockClient - intercept should return a MockInterceptor', (t) => { t.ok(interceptor instanceof MockInterceptor) }) -test('MockClient - intercept validation', (t) => { - t.plan(4) - - t.test('it should error if no options specified in the intercept', t => { - t.plan(1) +describe('MockClient - intercept validation', () => { + test('it should error if no options specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get('http://localhost:9999') t.throws(() => mockClient.intercept(), new InvalidArgumentError('opts must be an object')) }) - t.test('it should error if no path specified in the intercept', t => { - t.plan(1) + test('it should error if no path specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get('http://localhost:9999') t.throws(() => mockClient.intercept({}), new InvalidArgumentError('opts.path must be defined')) }) - t.test('it should default to GET if no method specified in the intercept', t => { - t.plan(1) + test('it should default to GET if no method specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get('http://localhost:9999') t.doesNotThrow(() => mockClient.intercept({ path: '/foo' })) }) - t.test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => { - t.plan(1) + test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() const mockClient = mockAgent.get('http://localhost:3000') - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) mockClient.intercept({ path: '/test', method: 'patch' }).reply(200, 'Hello!') - t.equal(mockClient[kDispatches][0].method, 'PATCH') + t.strictEqual(mockClient[kDispatches][0].method, 'PATCH') }) }) test('MockClient - close should run without error', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) mockClient[kDispatches] = [ @@ -196,11 +191,12 @@ test('MockClient - close should run without error', async (t) => { } ] - await t.resolves(mockClient.close()) + await mockClient.close() + t.ok(true, 'pass') }) test('MockClient - should be able to set as globalDispatcher', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -208,14 +204,14 @@ test('MockClient - should be able to set as globalDispatcher', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -229,14 +225,14 @@ test('MockClient - should be able to set as globalDispatcher', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockClient - should support query params', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -244,14 +240,14 @@ test('MockClient - should support query params', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -270,14 +266,14 @@ test('MockClient - should support query params', async (t) => { method: 'GET', query }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockClient - should intercept query params with hardcoded path', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -285,14 +281,14 @@ test('MockClient - should intercept query params with hardcoded path', async (t) t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -310,14 +306,14 @@ test('MockClient - should intercept query params with hardcoded path', async (t) method: 'GET', query }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockClient - should intercept query params regardless of key ordering', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -325,14 +321,14 @@ test('MockClient - should intercept query params regardless of key ordering', as t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -358,14 +354,14 @@ test('MockClient - should intercept query params regardless of key ordering', as method: 'GET', query }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockClient - should be able to use as a local dispatcher', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -373,14 +369,14 @@ test('MockClient - should be able to use as a local dispatcher', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -394,14 +390,14 @@ test('MockClient - should be able to use as a local dispatcher', async (t) => { method: 'GET', dispatcher: mockClient }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockClient - basic intercept with MockClient.request', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -409,14 +405,14 @@ test('MockClient - basic intercept with MockClient.request', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent({ connections: 1 }) - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockClient = mockAgent.get(baseUrl) t.ok(mockClient instanceof MockClient) @@ -435,12 +431,12 @@ test('MockClient - basic intercept with MockClient.request', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) diff --git a/test/mock-errors.js b/test/mock-errors.js index 1087f0cf588..58ec2b52b9f 100644 --- a/test/mock-errors.js +++ b/test/mock-errors.js @@ -1,32 +1,27 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { describe, test } = require('node:test') const { mockErrors, errors } = require('..') -test('mockErrors', (t) => { - t.plan(1) +describe('MockNotMatchedError', () => { + test('should implement an UndiciError', t => { + t = tspl(t, { plan: 4 }) - t.test('MockNotMatchedError', t => { - t.plan(2) - - t.test('should implement an UndiciError', t => { - t.plan(4) - - const mockError = new mockErrors.MockNotMatchedError() - t.ok(mockError instanceof errors.UndiciError) - t.same(mockError.name, 'MockNotMatchedError') - t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') - t.same(mockError.message, 'The request does not match any registered mock dispatches') - }) + const mockError = new mockErrors.MockNotMatchedError() + t.ok(mockError instanceof errors.UndiciError) + t.deepStrictEqual(mockError.name, 'MockNotMatchedError') + t.deepStrictEqual(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') + t.deepStrictEqual(mockError.message, 'The request does not match any registered mock dispatches') + }) - t.test('should set a custom message', t => { - t.plan(4) + test('should set a custom message', t => { + t = tspl(t, { plan: 4 }) - const mockError = new mockErrors.MockNotMatchedError('custom message') - t.ok(mockError instanceof errors.UndiciError) - t.same(mockError.name, 'MockNotMatchedError') - t.same(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') - t.same(mockError.message, 'custom message') - }) + const mockError = new mockErrors.MockNotMatchedError('custom message') + t.ok(mockError instanceof errors.UndiciError) + t.deepStrictEqual(mockError.name, 'MockNotMatchedError') + t.deepStrictEqual(mockError.code, 'UND_MOCK_ERR_MOCK_NOT_MATCHED') + t.deepStrictEqual(mockError.message, 'custom message') }) }) diff --git a/test/mock-interceptor-unused-assertions.js b/test/mock-interceptor-unused-assertions.js index 2fe8b824f9f..e6f5360a3f9 100644 --- a/test/mock-interceptor-unused-assertions.js +++ b/test/mock-interceptor-unused-assertions.js @@ -1,6 +1,7 @@ 'use strict' -const { test, beforeEach, afterEach } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, beforeEach, afterEach } = require('node:test') const { MockAgent, setGlobalDispatcher } = require('..') const PendingInterceptorsFormatter = require('../lib/mock/pending-interceptors-formatter') const util = require('../lib/core/util') @@ -41,12 +42,14 @@ function mockAgentWithOneInterceptor () { } test('1 pending interceptor', t => { - t.plan(2) - - const err = t.throws(() => mockAgentWithOneInterceptor().assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - - t.same(err.message, tableRowsAlignedToLeft - ? ` + t = tspl(t, { plan: 1 }) + + try { + mockAgentWithOneInterceptor().assertNoPendingInterceptors({ pendingInterceptorsFormatter }) + t.fail('Should have thrown') + } catch (err) { + t.deepStrictEqual(err.message, tableRowsAlignedToLeft + ? ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -55,7 +58,7 @@ test('1 pending interceptor', t => { │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim() - : ` + : ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -64,20 +67,22 @@ test('1 pending interceptor', t => { │ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim()) + } }) test('2 pending interceptors', t => { - t.plan(2) + t = tspl(t, { plan: 1 }) const withTwoInterceptors = mockAgentWithOneInterceptor() withTwoInterceptors .get(origin) .intercept({ method: 'get', path: '/some/path' }) .reply(204, 'OK') - const err = t.throws(() => withTwoInterceptors.assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - - t.same(err.message, tableRowsAlignedToLeft - ? ` + try { + withTwoInterceptors.assertNoPendingInterceptors({ pendingInterceptorsFormatter }) + } catch (err) { + t.deepStrictEqual(err.message, tableRowsAlignedToLeft + ? ` 2 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -87,7 +92,7 @@ test('2 pending interceptors', t => { │ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ '❌' │ 0 │ 1 │ └─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim() - : ` + : ` 2 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -97,10 +102,11 @@ test('2 pending interceptors', t => { │ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ '❌' │ 0 │ 1 │ └─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim()) + } }) test('Variations of persist(), times(), and pending status', async t => { - t.plan(7) + t = tspl(t, { plan: 6 }) // Agent with unused interceptor const agent = mockAgentWithOneInterceptor() @@ -118,20 +124,20 @@ test('Variations of persist(), times(), and pending status', async t => { .intercept({ method: 'GET', path: '/persistent/used' }) .reply(200, 'OK') .persist() - t.same((await agent.request({ origin, method: 'GET', path: '/persistent/used' })).statusCode, 200) + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/persistent/used' })).statusCode, 200) // Consumed without persist() agent.get(origin) .intercept({ method: 'post', path: '/transient/pending' }) .reply(201, 'Created') - t.same((await agent.request({ origin, method: 'POST', path: '/transient/pending' })).statusCode, 201) + t.deepStrictEqual((await agent.request({ origin, method: 'POST', path: '/transient/pending' })).statusCode, 201) // Partially pending with times() agent.get(origin) .intercept({ method: 'get', path: '/times/partial' }) .reply(200, 'OK') .times(5) - t.same((await agent.request({ origin, method: 'GET', path: '/times/partial' })).statusCode, 200) + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/partial' })).statusCode, 200) // Unused with times() agent.get(origin) @@ -144,13 +150,15 @@ test('Variations of persist(), times(), and pending status', async t => { .intercept({ method: 'get', path: '/times/pending' }) .reply(200, 'OK') .times(2) - t.same((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200) - t.same((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200) - - const err = t.throws(() => agent.assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - - t.same(err.message, tableRowsAlignedToLeft - ? ` + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200) + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/times/pending' })).statusCode, 200) + + try { + agent.assertNoPendingInterceptors({ pendingInterceptorsFormatter }) + t.fail('Should have thrown') + } catch (err) { + t.deepStrictEqual(err.message, tableRowsAlignedToLeft + ? ` 4 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -162,7 +170,7 @@ test('Variations of persist(), times(), and pending status', async t => { │ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ '❌' │ 0 │ 2 │ └─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim() - : ` + : ` 4 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -174,45 +182,48 @@ test('Variations of persist(), times(), and pending status', async t => { │ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ '❌' │ 0 │ 2 │ └─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim()) + } }) test('works when no interceptors are registered', t => { - t.plan(2) + t = tspl(t, { plan: 2 }) const agent = new MockAgent() agent.disableNetConnect() - t.same(agent.pendingInterceptors(), []) + t.deepStrictEqual(agent.pendingInterceptors(), []) t.doesNotThrow(() => agent.assertNoPendingInterceptors()) }) test('works when all interceptors are pending', async t => { - t.plan(4) + t = tspl(t, { plan: 4 }) const agent = new MockAgent() agent.disableNetConnect() agent.get(origin).intercept({ method: 'get', path: '/' }).reply(200, 'OK') - t.same((await agent.request({ origin, method: 'GET', path: '/' })).statusCode, 200) + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/' })).statusCode, 200) agent.get(origin).intercept({ method: 'get', path: '/persistent' }).reply(200, 'OK') - t.same((await agent.request({ origin, method: 'GET', path: '/persistent' })).statusCode, 200) + t.deepStrictEqual((await agent.request({ origin, method: 'GET', path: '/persistent' })).statusCode, 200) - t.same(agent.pendingInterceptors(), []) + t.deepStrictEqual(agent.pendingInterceptors(), []) t.doesNotThrow(() => agent.assertNoPendingInterceptors()) }) test('defaults to rendering output with terminal color when process.env.CI is unset', t => { - t.plan(2) + t = tspl(t, { plan: 1 }) // This ensures that the test works in an environment where the CI env var is set. const oldCiEnvVar = process.env.CI delete process.env.CI - const err = t.throws( - () => mockAgentWithOneInterceptor().assertNoPendingInterceptors()) - t.same(err.message, tableRowsAlignedToLeft - ? ` + try { + mockAgentWithOneInterceptor().assertNoPendingInterceptors() + t.fail('Shoudl have thrown') + } catch (err) { + t.deepStrictEqual(err.message, tableRowsAlignedToLeft + ? ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -221,7 +232,7 @@ test('defaults to rendering output with terminal color when process.env.CI is un │ 0 │ \u001b[32m'GET'\u001b[39m │ \u001b[32m'https://example.com'\u001b[39m │ \u001b[32m'/'\u001b[39m │ \u001b[33m200\u001b[39m │ \u001b[32m'❌'\u001b[39m │ \u001b[33m0\u001b[39m │ \u001b[33m1\u001b[39m │ └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim() - : ` + : ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -231,19 +242,20 @@ test('defaults to rendering output with terminal color when process.env.CI is un └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ `.trim()) - // Re-set the CI env var if it were set. - // Assigning `undefined` does not work, - // because reading the env var afterwards yields the string 'undefined', - // so we need to re-set it conditionally. - if (oldCiEnvVar != null) { - process.env.CI = oldCiEnvVar + // Re-set the CI env var if it were set. + // Assigning `undefined` does not work, + // because reading the env var afterwards yields the string 'undefined', + // so we need to re-set it conditionally. + if (oldCiEnvVar != null) { + process.env.CI = oldCiEnvVar + } } }) test('returns unused interceptors', t => { - t.plan(1) + t = tspl(t, { plan: 1 }) - t.same(mockAgentWithOneInterceptor().pendingInterceptors(), [ + t.deepStrictEqual(mockAgentWithOneInterceptor().pendingInterceptors(), [ { timesInvoked: 0, times: 1, diff --git a/test/mock-interceptor.js b/test/mock-interceptor.js index 787878395f4..036ea69df98 100644 --- a/test/mock-interceptor.js +++ b/test/mock-interceptor.js @@ -1,28 +1,26 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { describe, test, after } = require('node:test') const { MockInterceptor, MockScope } = require('../lib/mock/mock-interceptor') const MockAgent = require('../lib/mock/mock-agent') const { kDispatchKey } = require('../lib/mock/mock-symbols') const { InvalidArgumentError } = require('../lib/core/errors') -test('MockInterceptor - path', t => { - t.plan(1) - t.test('should remove hash fragment from paths', t => { - t.plan(1) +describe('MockInterceptor - path', () => { + test('should remove hash fragment from paths', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '#foobar', method: '' }, []) - t.equal(mockInterceptor[kDispatchKey].path, '') + t.strictEqual(mockInterceptor[kDispatchKey].path, '') }) }) -test('MockInterceptor - reply', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockInterceptor - reply', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' @@ -31,8 +29,8 @@ test('MockInterceptor - reply', t => { t.ok(result instanceof MockScope) }) - t.test('should error if passed options invalid', t => { - t.plan(2) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 2 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -43,11 +41,9 @@ test('MockInterceptor - reply', t => { }) }) -test('MockInterceptor - reply callback', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockInterceptor - reply callback', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' @@ -56,23 +52,21 @@ test('MockInterceptor - reply callback', t => { t.ok(result instanceof MockScope) }) - t.test('should error if passed options invalid', t => { - t.plan(2) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 2 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' }, []) t.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined')) - t.throws(() => mockInterceptor.reply(200, () => {}, 'hello'), new InvalidArgumentError('responseOptions must be an object')) + t.throws(() => mockInterceptor.reply(200, () => { }, 'hello'), new InvalidArgumentError('responseOptions must be an object')) }) }) -test('MockInterceptor - reply options callback', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(2) +describe('MockInterceptor - reply options callback', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 2 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -88,7 +82,7 @@ test('MockInterceptor - reply options callback', t => { const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -96,7 +90,7 @@ test('MockInterceptor - reply options callback', t => { path: '/test', method: 'GET' }).reply((options) => { - t.strictSame(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } }) + t.deepStrictEqual(options, { path: '/test', method: 'GET', headers: { foo: 'bar' } }) return { statusCode: 200, data: 'hello' } }) @@ -105,25 +99,25 @@ test('MockInterceptor - reply options callback', t => { method: 'GET', headers: { foo: 'bar' } }, { - onHeaders: () => {}, - onData: () => {}, - onComplete: () => {} + onHeaders: () => { }, + onData: () => { }, + onComplete: () => { } }) }) - t.test('should error if passed options invalid', async (t) => { - t.plan(3) + test('should error if passed options invalid', async (t) => { + t = tspl(t, { plan: 3 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) mockPool.intercept({ path: '/test', method: 'GET' - }).reply(() => {}) + }).reply(() => { }) mockPool.intercept({ path: '/test3', @@ -146,36 +140,34 @@ test('MockInterceptor - reply options callback', t => { path: '/test', method: 'GET' }, { - onHeaders: () => {}, - onData: () => {}, - onComplete: () => {} + onHeaders: () => { }, + onData: () => { }, + onComplete: () => { } }), new InvalidArgumentError('reply options callback must return an object')) t.throws(() => mockPool.dispatch({ path: '/test3', method: 'GET' }, { - onHeaders: () => {}, - onData: () => {}, - onComplete: () => {} + onHeaders: () => { }, + onData: () => { }, + onComplete: () => { } }), new InvalidArgumentError('responseOptions must be an object')) t.throws(() => mockPool.dispatch({ path: '/test4', method: 'GET' }, { - onHeaders: () => {}, - onData: () => {}, - onComplete: () => {} + onHeaders: () => { }, + onData: () => { }, + onComplete: () => { } }), new InvalidArgumentError('statusCode must be defined')) }) }) -test('MockInterceptor - replyWithError', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockInterceptor - replyWithError', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' @@ -184,8 +176,8 @@ test('MockInterceptor - replyWithError', t => { t.ok(result instanceof MockScope) }) - t.test('should error if passed options invalid', t => { - t.plan(1) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -195,11 +187,9 @@ test('MockInterceptor - replyWithError', t => { }) }) -test('MockInterceptor - defaultReplyHeaders', t => { - t.plan(2) - - t.test('should return MockInterceptor', t => { - t.plan(1) +describe('MockInterceptor - defaultReplyHeaders', () => { + test('should return MockInterceptor', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' @@ -208,8 +198,8 @@ test('MockInterceptor - defaultReplyHeaders', t => { t.ok(result instanceof MockInterceptor) }) - t.test('should error if passed options invalid', t => { - t.plan(1) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -219,11 +209,9 @@ test('MockInterceptor - defaultReplyHeaders', t => { }) }) -test('MockInterceptor - defaultReplyTrailers', t => { - t.plan(2) - - t.test('should return MockInterceptor', t => { - t.plan(1) +describe('MockInterceptor - defaultReplyTrailers', () => { + test('should return MockInterceptor', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' @@ -232,8 +220,8 @@ test('MockInterceptor - defaultReplyTrailers', t => { t.ok(result instanceof MockInterceptor) }) - t.test('should error if passed options invalid', t => { - t.plan(1) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -243,11 +231,9 @@ test('MockInterceptor - defaultReplyTrailers', t => { }) }) -test('MockInterceptor - replyContentLength', t => { - t.plan(1) - - t.test('should return MockInterceptor', t => { - t.plan(1) +describe('MockInterceptor - replyContentLength', () => { + test('should return MockInterceptor', t => { + t = tspl(t, { plan: 1 }) const mockInterceptor = new MockInterceptor({ path: '', method: '' diff --git a/test/mock-pool.js b/test/mock-pool.js index ca812d90866..0b690fe6d48 100644 --- a/test/mock-pool.js +++ b/test/mock-pool.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { createServer } = require('node:http') const { promisify } = require('node:util') const { MockAgent, MockPool, getGlobalDispatcher, setGlobalDispatcher, request } = require('..') @@ -12,37 +13,33 @@ const { getResponse } = require('../lib/mock/mock-utils') const Dispatcher = require('../lib/dispatcher') const { fetch } = require('..') -test('MockPool - constructor', t => { - t.plan(3) - - t.test('fails if opts.agent does not implement `get` method', t => { - t.plan(1) +describe('MockPool - constructor', () => { + test('fails if opts.agent does not implement `get` method', t => { + t = tspl(t, { plan: 1 }) t.throws(() => new MockPool('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError) }) - t.test('sets agent', t => { - t.plan(1) + test('sets agent', t => { + t = tspl(t, { plan: 1 }) t.doesNotThrow(() => new MockPool('http://localhost:9999', { agent: new MockAgent() })) }) - t.test('should implement the Dispatcher API', t => { - t.plan(1) + test('should implement the Dispatcher API', t => { + t = tspl(t, { plan: 1 }) const mockPool = new MockPool('http://localhost:9999', { agent: new MockAgent() }) t.ok(mockPool instanceof Dispatcher) }) }) -test('MockPool - dispatch', t => { - t.plan(2) - - t.test('should handle a single interceptor', (t) => { - t.plan(1) +describe('MockPool - dispatch', () => { + test('should handle a single interceptor', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -66,18 +63,18 @@ test('MockPool - dispatch', t => { method: 'GET' }, { onHeaders: (_statusCode, _headers, resume) => resume(), - onData: () => {}, - onComplete: () => {} + onData: () => { }, + onComplete: () => { } })) }) - t.test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => { - t.plan(1) + test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => { + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -101,19 +98,19 @@ test('MockPool - dispatch', t => { method: 'GET' }, { onHeaders: (_statusCode, _headers, resume) => { throw new Error('kaboom') }, - onData: () => {}, - onComplete: () => {} + onData: () => { }, + onComplete: () => { } }), new Error('kaboom')) }) }) test('MockPool - intercept should return a MockInterceptor', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -125,33 +122,31 @@ test('MockPool - intercept should return a MockInterceptor', (t) => { t.ok(interceptor instanceof MockInterceptor) }) -test('MockPool - intercept validation', (t) => { - t.plan(3) - - t.test('it should error if no options specified in the intercept', t => { - t.plan(1) +describe('MockPool - intercept validation', () => { + test('it should error if no options specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:9999') t.throws(() => mockPool.intercept(), new InvalidArgumentError('opts must be an object')) }) - t.test('it should error if no path specified in the intercept', t => { - t.plan(1) + test('it should error if no path specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:9999') t.throws(() => mockPool.intercept({}), new InvalidArgumentError('opts.path must be defined')) }) - t.test('it should default to GET if no method specified in the intercept', t => { - t.plan(1) + test('it should default to GET if no method specified in the intercept', t => { + t = tspl(t, { plan: 1 }) const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get('http://localhost:9999') t.doesNotThrow(() => mockPool.intercept({ path: '/foo' })) @@ -159,12 +154,12 @@ test('MockPool - intercept validation', (t) => { }) test('MockPool - close should run without error', async (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const baseUrl = 'http://localhost:9999' const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) @@ -182,11 +177,12 @@ test('MockPool - close should run without error', async (t) => { } ] - await t.resolves(mockPool.close()) + await mockPool.close() + t.ok(true, 'pass') }) test('MockPool - should be able to set as globalDispatcher', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -194,14 +190,14 @@ test('MockPool - should be able to set as globalDispatcher', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) t.ok(mockPool instanceof MockPool) @@ -215,14 +211,14 @@ test('MockPool - should be able to set as globalDispatcher', async (t) => { const { statusCode, body } = await request(`${baseUrl}/foo`, { method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockPool - should be able to use as a local dispatcher', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -230,14 +226,14 @@ test('MockPool - should be able to use as a local dispatcher', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) t.ok(mockPool instanceof MockPool) @@ -251,14 +247,14 @@ test('MockPool - should be able to use as a local dispatcher', async (t) => { method: 'GET', dispatcher: mockPool }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const response = await getResponse(body) - t.same(response, 'hello') + t.deepStrictEqual(response, 'hello') }) test('MockPool - basic intercept with MockPool.request', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') @@ -266,14 +262,14 @@ test('MockPool - basic intercept with MockPool.request', async (t) => { t.fail('should not be called') t.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const baseUrl = `http://localhost:${server.address().port}` const mockAgent = new MockAgent() - t.teardown(mockAgent.close.bind(mockAgent)) + after(() => mockAgent.close()) const mockPool = mockAgent.get(baseUrl) t.ok(mockPool instanceof MockPool) @@ -292,25 +288,27 @@ test('MockPool - basic intercept with MockPool.request', async (t) => { method: 'POST', body: 'form1=data1&form2=data2' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'application/json') - t.same(trailers, { 'content-md5': 'test' }) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'application/json') + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) const jsonResponse = JSON.parse(await getResponse(body)) - t.same(jsonResponse, { + t.deepStrictEqual(jsonResponse, { foo: 'bar' }) }) // https://github.com/nodejs/undici/issues/1546 test('MockPool - correct errors when consuming invalid JSON body', async (t) => { + t = tspl(t, { plan: 1 }) + const oldDispatcher = getGlobalDispatcher() const mockAgent = new MockAgent() mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) - t.teardown(() => setGlobalDispatcher(oldDispatcher)) + after(() => setGlobalDispatcher(oldDispatcher)) const mockPool = mockAgent.get('https://google.com') mockPool.intercept({ @@ -324,6 +322,8 @@ test('MockPool - correct errors when consuming invalid JSON body', async (t) => }) test('MockPool - allows matching headers in fetch', async (t) => { + t = tspl(t, { plan: 2 }) + const oldDispatcher = getGlobalDispatcher() const baseUrl = 'http://localhost:9999' @@ -331,7 +331,7 @@ test('MockPool - allows matching headers in fetch', async (t) => { mockAgent.disableNetConnect() setGlobalDispatcher(mockAgent) - t.teardown(async () => { + after(async () => { await mockAgent.close() setGlobalDispatcher(oldDispatcher) }) @@ -345,23 +345,19 @@ test('MockPool - allows matching headers in fetch', async (t) => { } }).reply(200, { ok: 1 }).times(3) - await t.resolves( - fetch(`${baseUrl}/foo`, { - headers: { - accept: 'application/json' - } - }) - ) + await fetch(`${baseUrl}/foo`, { + headers: { + accept: 'application/json' + } + }) // no 'accept: application/json' header sent, not matched await t.rejects(fetch(`${baseUrl}/foo`)) // not 'accept: application/json', not matched - await t.rejects(fetch(`${baseUrl}/foo`), { + await t.rejects(fetch(`${baseUrl}/foo`, { headers: { accept: 'text/plain' } - }, TypeError) - - t.end() + }), new TypeError('fetch failed')) }) diff --git a/test/mock-scope.js b/test/mock-scope.js index d8f41d90198..0344b99d0bc 100644 --- a/test/mock-scope.js +++ b/test/mock-scope.js @@ -1,14 +1,13 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, describe } = require('node:test') const { MockScope } = require('../lib/mock/mock-interceptor') const { InvalidArgumentError } = require('../lib/core/errors') -test('MockScope - delay', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockScope - delay', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockScope = new MockScope({ path: '', method: '' @@ -17,8 +16,8 @@ test('MockScope - delay', t => { t.ok(result instanceof MockScope) }) - t.test('should error if passed options invalid', t => { - t.plan(4) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 4 }) const mockScope = new MockScope({ path: '', @@ -31,11 +30,9 @@ test('MockScope - delay', t => { }) }) -test('MockScope - persist', t => { - t.plan(1) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockScope - persist', () => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockScope = new MockScope({ path: '', method: '' @@ -45,11 +42,9 @@ test('MockScope - persist', t => { }) }) -test('MockScope - times', t => { - t.plan(2) - - t.test('should return MockScope', t => { - t.plan(1) +describe('MockScope - times', t => { + test('should return MockScope', t => { + t = tspl(t, { plan: 1 }) const mockScope = new MockScope({ path: '', method: '' @@ -58,8 +53,8 @@ test('MockScope - times', t => { t.ok(result instanceof MockScope) }) - t.test('should error if passed options invalid', t => { - t.plan(4) + test('should error if passed options invalid', t => { + t = tspl(t, { plan: 4 }) const mockScope = new MockScope({ path: '', diff --git a/test/mock-utils.js b/test/mock-utils.js index 4acc8558390..baf0933ba57 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, describe } = require('node:test') const { MockNotMatchedError } = require('../lib/mock/mock-errors') const { deleteMockDispatch, @@ -11,7 +12,7 @@ const { } = require('../lib/mock/mock-utils') test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const key = { url: 'url', @@ -23,11 +24,9 @@ test('deleteMockDispatch - should do nothing if not able to find mock dispatch', t.doesNotThrow(() => deleteMockDispatch([], key)) }) -test('getMockDispatch', (t) => { - t.plan(3) - - t.test('it should find a mock dispatch', (t) => { - t.plan(1) +describe('getMockDispatch', () => { + test('it should find a mock dispatch', (t) => { + t = tspl(t, { plan: 1 }) const dispatches = [ { path: 'path', @@ -40,15 +39,15 @@ test('getMockDispatch', (t) => { path: 'path', method: 'method' }) - t.same(result, { + t.deepStrictEqual(result, { path: 'path', method: 'method', consumed: false }) }) - t.test('it should skip consumed dispatches', (t) => { - t.plan(1) + test('it should skip consumed dispatches', (t) => { + t = tspl(t, { plan: 1 }) const dispatches = [ { path: 'path', @@ -66,15 +65,15 @@ test('getMockDispatch', (t) => { path: 'path', method: 'method' }) - t.same(result, { + t.deepStrictEqual(result, { path: 'path', method: 'method', consumed: false }) }) - t.test('it should throw if dispatch not found', (t) => { - t.plan(1) + test('it should throw if dispatch not found', (t) => { + t = tspl(t, { plan: 1 }) const dispatches = [ { path: 'path', @@ -90,29 +89,29 @@ test('getMockDispatch', (t) => { }) }) -test('getResponseData', (t) => { - t.plan(3) - - t.test('it should stringify objects', (t) => { - t.plan(1) +describe('getResponseData', () => { + test('it should stringify objects', (t) => { + t = tspl(t, { plan: 1 }) const responseData = getResponseData({ str: 'string', num: 42 }) - t.equal(responseData, '{"str":"string","num":42}') + t.strictEqual(responseData, '{"str":"string","num":42}') }) - t.test('it should return strings untouched', (t) => { - t.plan(1) + test('it should return strings untouched', (t) => { + t = tspl(t, { plan: 1 }) const responseData = getResponseData('test') - t.equal(responseData, 'test') + t.strictEqual(responseData, 'test') }) - t.test('it should return buffers untouched', (t) => { - t.plan(1) + test('it should return buffers untouched', (t) => { + t = tspl(t, { plan: 1 }) const responseData = getResponseData(Buffer.from('test')) t.ok(Buffer.isBuffer(responseData)) }) }) test('getStatusText', (t) => { + t = tspl(t, { plan: 64 }) + for (const statusCode of [ 100, 101, 102, 103, 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, @@ -126,23 +125,25 @@ test('getStatusText', (t) => { t.ok(getStatusText(statusCode)) } - t.equal(getStatusText(420), 'unknown') + t.strictEqual(getStatusText(420), 'unknown') t.end() }) test('getHeaderByName', (t) => { + t = tspl(t, { plan: 6 }) + const headersRecord = { key: 'value' } - t.equal(getHeaderByName(headersRecord, 'key'), 'value') - t.equal(getHeaderByName(headersRecord, 'anotherKey'), undefined) + t.strictEqual(getHeaderByName(headersRecord, 'key'), 'value') + t.strictEqual(getHeaderByName(headersRecord, 'anotherKey'), undefined) const headersArray = ['key', 'value'] - t.equal(getHeaderByName(headersArray, 'key'), 'value') - t.equal(getHeaderByName(headersArray, 'anotherKey'), undefined) + t.strictEqual(getHeaderByName(headersArray, 'key'), 'value') + t.strictEqual(getHeaderByName(headersArray, 'anotherKey'), undefined) const { Headers } = require('../index') @@ -150,8 +151,8 @@ test('getHeaderByName', (t) => { ['key', 'value'] ]) - t.equal(getHeaderByName(headers, 'key'), 'value') - t.equal(getHeaderByName(headers, 'anotherKey'), null) + t.strictEqual(getHeaderByName(headers, 'key'), 'value') + t.strictEqual(getHeaderByName(headers, 'anotherKey'), null) t.end() }) From e49471f6258a0c73e97f697e97543daa0d824639 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:02:05 +0100 Subject: [PATCH 020/123] chore: migrate a batch of tests to node test runner (#2740) --- test/redirect-pipeline.js | 27 +-- test/redirect-relative.js | 13 +- test/redirect-request.js | 325 +++++++++++++++++++----------- test/redirect-stream.js | 220 ++++++++++---------- test/redirect-upgrade.js | 15 +- test/utils/redirecting-servers.js | 41 ++-- 6 files changed, 365 insertions(+), 276 deletions(-) diff --git a/test/redirect-pipeline.js b/test/redirect-pipeline.js index d958ee513f1..98896fae19c 100644 --- a/test/redirect-pipeline.js +++ b/test/redirect-pipeline.js @@ -1,6 +1,7 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { pipeline: undiciPipeline } = require('..') const { pipeline: streamPipelineCb } = require('node:stream') const { promisify } = require('node:util') @@ -9,42 +10,42 @@ const { startRedirectingServer } = require('./utils/redirecting-servers') const streamPipeline = promisify(streamPipelineCb) -t.test('should not follow redirection by default if not using RedirectAgent', async t => { - t.plan(3) +test('should not follow redirection by default if not using RedirectAgent', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const serverRoot = await startRedirectingServer(t) + const serverRoot = await startRedirectingServer() await streamPipeline( createReadable('REQUEST'), undiciPipeline(`http://${serverRoot}/`, {}, ({ statusCode, headers, body }) => { - t.equal(statusCode, 302) - t.equal(headers.location, `http://${serverRoot}/302/1`) + t.strictEqual(statusCode, 302) + t.strictEqual(headers.location, `http://${serverRoot}/302/1`) return body }), createWritable(body) ) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) -t.test('should not follow redirects when using RedirectAgent within pipeline', async t => { - t.plan(3) +test('should not follow redirects when using RedirectAgent within pipeline', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const serverRoot = await startRedirectingServer(t) + const serverRoot = await startRedirectingServer() await streamPipeline( createReadable('REQUEST'), undiciPipeline(`http://${serverRoot}/`, { maxRedirections: 1 }, ({ statusCode, headers, body }) => { - t.equal(statusCode, 302) - t.equal(headers.location, `http://${serverRoot}/302/1`) + t.strictEqual(statusCode, 302) + t.strictEqual(headers.location, `http://${serverRoot}/302/1`) return body }), createWritable(body) ) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) diff --git a/test/redirect-relative.js b/test/redirect-relative.js index ca9c5411ba4..e4d45406e75 100644 --- a/test/redirect-relative.js +++ b/test/redirect-relative.js @@ -1,15 +1,16 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { request } = require('..') const { startRedirectingWithRelativePath } = require('./utils/redirecting-servers') -t.test('should redirect to relative URL according to RFC 7231', async t => { - t.plan(2) +test('should redirect to relative URL according to RFC 7231', async t => { + t = tspl(t, { plan: 2 }) - const server = await startRedirectingWithRelativePath(t) + const server = await startRedirectingWithRelativePath() const { statusCode, body } = await request(`http://${server}`, { maxRedirections: 3 @@ -17,6 +18,6 @@ t.test('should redirect to relative URL according to RFC 7231', async t => { const finalPath = await body.text() - t.equal(statusCode, 200) - t.equal(finalPath, '/absolute/b') + t.strictEqual(statusCode, 200) + t.strictEqual(finalPath, '/absolute/b') }) diff --git a/test/redirect-request.js b/test/redirect-request.js index d200ec17aa8..e7554b929f6 100644 --- a/test/redirect-request.js +++ b/test/redirect-request.js @@ -1,6 +1,7 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const undici = require('..') const { startRedirectingServer, @@ -20,12 +21,14 @@ for (const factory of [ ]) { const request = (t, server, opts, ...args) => { const dispatcher = factory(server, opts) - t.teardown(() => dispatcher.close()) + after(() => dispatcher.close()) return undici.request(args[0], { ...args[1], dispatcher }, args[2]) } - t.test('should always have a history with the final URL even if no redirections were followed', async t => { - const server = await startRedirectingServer(t) + test('should always have a history with the final URL even if no redirections were followed', async t => { + t = tspl(t, { plan: 4 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/200?key=value`, { maxRedirections: 10 @@ -33,25 +36,33 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [`http://${server}/200?key=value`]) - t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/200?key=value`]) + t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should not follow redirection by default if not using RedirectAgent', async t => { - const server = await startRedirectingServer(t) + test('should not follow redirection by default if not using RedirectAgent', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}`) const body = await bodyStream.text() - t.equal(statusCode, 302) - t.equal(headers.location, `http://${server}/302/1`) - t.equal(body.length, 0) + t.strictEqual(statusCode, 302) + t.strictEqual(headers.location, `http://${server}/302/1`) + t.strictEqual(body.length, 0) + + await t.completed }) - t.test('should follow redirection after a HTTP 300', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 300', async t => { + t = tspl(t, { plan: 4 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, { maxRedirections: 10 @@ -59,9 +70,9 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server}/300?key=value`, `http://${server}/300/1?key=value`, `http://${server}/300/2?key=value`, @@ -69,18 +80,22 @@ for (const factory of [ `http://${server}/300/4?key=value`, `http://${server}/300/5?key=value` ]) - t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should follow redirection after a HTTP 300 default', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 300 default', async t => { + t = tspl(t, { plan: 4 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, { maxRedirections: 10 }, `http://${server}/300?key=value`) const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server}/300?key=value`, `http://${server}/300/1?key=value`, `http://${server}/300/2?key=value`, @@ -88,11 +103,15 @@ for (const factory of [ `http://${server}/300/4?key=value`, `http://${server}/300/5?key=value` ]) - t.equal(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + t.strictEqual(body, `GET /5 key=value :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should follow redirection after a HTTP 301', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 301', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', @@ -102,13 +121,14 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) }) - t.test('should follow redirection after a HTTP 302', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 302', async t => { + t = tspl(t, { plan: 3 }) + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, { method: 'PUT', @@ -118,13 +138,15 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) }) - t.test('should follow redirection after a HTTP 303 changing method to GET', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 303 changing method to GET', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', @@ -134,13 +156,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `GET /5 :: host@${server} connection@keep-alive`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { - const server = await startRedirectingServer(t) + test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', @@ -165,13 +191,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + + await t.completed }) - t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { - const server = await startRedirectingServer(t) + test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', @@ -189,13 +219,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + + await t.completed }) - t.test('should follow redirection after a HTTP 307', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 307', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/307`, { method: 'DELETE', @@ -204,13 +238,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `DELETE /5 :: host@${server} connection@keep-alive`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `DELETE /5 :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should follow redirection after a HTTP 308', async t => { - const server = await startRedirectingServer(t) + test('should follow redirection after a HTTP 308', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/308`, { method: 'OPTIONS', @@ -219,13 +257,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.equal(body, `OPTIONS /5 :: host@${server} connection@keep-alive`) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.strictEqual(body, `OPTIONS /5 :: host@${server} connection@keep-alive`) + + await t.completed }) - t.test('should ignore HTTP 3xx response bodies', async t => { - const server = await startRedirectingWithBodyServer(t) + test('should ignore HTTP 3xx response bodies', async t => { + t = tspl(t, { plan: 4 }) + + const server = await startRedirectingWithBodyServer() const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/`, { maxRedirections: 10 @@ -233,27 +275,35 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`]) - t.equal(body, 'FINAL') + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`]) + t.strictEqual(body, 'FINAL') + + await t.completed }) - t.test('should ignore query after redirection', async t => { - const server = await startRedirectingWithQueryParams(t) + test('should ignore query after redirection', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingWithQueryParams() const { statusCode, headers, context: { history } } = await request(t, server, undefined, `http://${server}/`, { maxRedirections: 10, query: { param1: 'first' } }) - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/?param2=second`]) + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/?param2=second`]) + + await t.completed }) - t.test('should follow a redirect chain up to the allowed number of times', async t => { - const server = await startRedirectingServer(t) + test('should follow a redirect chain up to the allowed number of times', async t => { + t = tspl(t, { plan: 4 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, { maxRedirections: 2 @@ -261,27 +311,24 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 300) - t.equal(headers.location, `http://${server}/300/3`) - t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`]) - t.equal(body.length, 0) + t.strictEqual(statusCode, 300) + t.strictEqual(headers.location, `http://${server}/300/3`) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`]) + t.strictEqual(body.length, 0) + + await t.completed }) - t.test('should follow a redirect chain up to the allowed number of times for redirectionLimitReached', async t => { - const server = await startRedirectingServer(t) + test('should follow a redirect chain up to the allowed number of times for redirectionLimitReached', async t => { + t = tspl(t, { plan: 1 }) + + const server = await startRedirectingServer() try { - const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, { + await request(t, server, undefined, `http://${server}/300`, { maxRedirections: 2, throwOnMaxRedirect: true }) - - const body = await bodyStream.text() - - t.equal(statusCode, 300) - t.equal(headers.location, `http://${server}/300/2`) - t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`]) - t.equal(body.length, 0) } catch (error) { if (error.message.startsWith('max redirects')) { t.ok(true, 'Max redirects handled correctly') @@ -289,11 +336,15 @@ for (const factory of [ t.fail(`Unexpected error: ${error.message}`) } } + + await t.completed }) - t.test('when a Location response header is NOT present', async t => { + test('when a Location response header is NOT present', async t => { + t = tspl(t, { plan: 6 * 3 }) + const redirectCodes = [300, 301, 302, 303, 307, 308] - const server = await startRedirectingWithoutLocationServer(t) + const server = await startRedirectingWithoutLocationServer() for (const code of redirectCodes) { const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/${code}`, { @@ -302,13 +353,16 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, code) - t.notOk(headers.location) - t.equal(body.length, 0) + t.strictEqual(statusCode, code) + t.ok(!headers.location) + t.strictEqual(body.length, 0) } + await t.completed }) - t.test('should not allow invalid maxRedirections arguments', async t => { + test('should not allow invalid maxRedirections arguments', async t => { + t = tspl(t, { plan: 1 }) + try { await request(t, 'localhost', undefined, 'http://localhost', { method: 'GET', @@ -317,11 +371,14 @@ for (const factory of [ t.fail('Did not throw') } catch (err) { - t.equal(err.message, 'maxRedirections must be a positive number') + t.strictEqual(err.message, 'maxRedirections must be a positive number') } + await t.completed }) - t.test('should not allow invalid maxRedirections arguments default', async t => { + test('should not allow invalid maxRedirections arguments default', async t => { + t = tspl(t, { plan: 1 }) + try { await request(t, 'localhost', { maxRedirections: 'INVALID' @@ -331,12 +388,16 @@ for (const factory of [ t.fail('Did not throw') } catch (err) { - t.equal(err.message, 'maxRedirections must be a positive number') + t.strictEqual(err.message, 'maxRedirections must be a positive number') } + + await t.completed }) - t.test('should not follow redirects when using ReadableStream request bodies', async t => { - const server = await startRedirectingServer(t) + test('should not follow redirects when using ReadableStream request bodies', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', @@ -346,13 +407,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 301) - t.equal(headers.location, `http://${server}/301/2`) - t.equal(body.length, 0) + t.strictEqual(statusCode, 301) + t.strictEqual(headers.location, `http://${server}/301/2`) + t.strictEqual(body.length, 0) + + await t.completed }) - t.test('should not follow redirects when using Readable request bodies', async t => { - const server = await startRedirectingServer(t) + test('should not follow redirects when using Readable request bodies', async t => { + t = tspl(t, { plan: 3 }) + + const server = await startRedirectingServer() const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', @@ -362,14 +427,17 @@ for (const factory of [ const body = await bodyStream.text() - t.equal(statusCode, 301) - t.equal(headers.location, `http://${server}/301/1`) - t.equal(body.length, 0) + t.strictEqual(statusCode, 301) + t.strictEqual(headers.location, `http://${server}/301/1`) + t.strictEqual(body.length, 0) + await t.completed }) } -t.test('should follow redirections when going cross origin', async t => { - const [server1, server2, server3] = await startRedirectingChainServers(t) +test('should follow redirections when going cross origin', async t => { + t = tspl(t, { plan: 4 }) + + const [server1, server2, server3] = await startRedirectingChainServers() const { statusCode, headers, body: bodyStream, context: { history } } = await undici.request(`http://${server1}`, { method: 'POST', @@ -378,9 +446,9 @@ t.test('should follow redirections when going cross origin', async t => { const body = await bodyStream.text() - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.ok(!headers.location) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server1}/`, `http://${server2}/`, `http://${server3}/`, @@ -388,11 +456,13 @@ t.test('should follow redirections when going cross origin', async t => { `http://${server3}/end`, `http://${server1}/end` ]) - t.equal(body, 'POST') + t.strictEqual(body, 'POST') + + await t.completed }) -t.test('should handle errors (callback)', t => { - t.plan(1) +test('should handle errors (callback)', async t => { + t = tspl(t, { plan: 1 }) undici.request( 'http://localhost:0', @@ -403,19 +473,27 @@ t.test('should handle errors (callback)', t => { t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/) } ) + + await t.completed }) -t.test('should handle errors (promise)', async t => { +test('should handle errors (promise)', async t => { + t = tspl(t, { plan: 1 }) + try { await undici.request('http://localhost:0', { maxRedirections: 10 }) t.fail('Did not throw') } catch (error) { t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/) } + + await t.completed }) -t.test('removes authorization header on third party origin', async t => { - const [server1] = await startRedirectingWithAuthorization(t, 'secret') +test('removes authorization header on third party origin', async t => { + t = tspl(t, { plan: 1 }) + + const [server1] = await startRedirectingWithAuthorization('secret') const { body: bodyStream } = await undici.request(`http://${server1}`, { maxRedirections: 10, headers: { @@ -425,11 +503,14 @@ t.test('removes authorization header on third party origin', async t => { const body = await bodyStream.text() - t.equal(body, '') + t.strictEqual(body, '') + + await t.completed }) -t.test('removes cookie header on third party origin', async t => { - const [server1] = await startRedirectingWithCookie(t, 'a=b') +test('removes cookie header on third party origin', async t => { + t = tspl(t, { plan: 1 }) + const [server1] = await startRedirectingWithCookie('a=b') const { body: bodyStream } = await undici.request(`http://${server1}`, { maxRedirections: 10, headers: { @@ -439,5 +520,7 @@ t.test('removes cookie header on third party origin', async t => { const body = await bodyStream.text() - t.equal(body, '') + t.strictEqual(body, '') + + await t.completed }) diff --git a/test/redirect-stream.js b/test/redirect-stream.js index 55dd97beb49..c50e5033135 100644 --- a/test/redirect-stream.js +++ b/test/redirect-stream.js @@ -1,6 +1,7 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, describe } = require('node:test') const { stream } = require('..') const { startRedirectingServer, @@ -12,19 +13,19 @@ const { } = require('./utils/redirecting-servers') const { createReadable, createWritable } = require('./utils/stream') -t.test('should always have a history with the final URL even if no redirections were followed', async t => { - t.plan(4) +test('should always have a history with the final URL even if no redirections were followed', async t => { + t = tspl(t, { plan: 4 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/200?key=value`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque, context: { history } }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server}/200?key=value` ]) @@ -32,38 +33,38 @@ t.test('should always have a history with the final URL even if no redirections } ) - t.equal(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) + t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) }) -t.test('should not follow redirection by default if not using RedirectAgent', async t => { - t.plan(3) +test('should not follow redirection by default if not using RedirectAgent', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream(`http://${server}`, { opaque: body }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 302) - t.equal(headers.location, `http://${server}/302/1`) + t.strictEqual(statusCode, 302) + t.strictEqual(headers.location, `http://${server}/302/1`) return createWritable(opaque) }) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) -t.test('should follow redirection after a HTTP 300', async t => { - t.plan(4) +test('should follow redirection after a HTTP 300', async t => { + t = tspl(t, { plan: 4 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/300?key=value`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque, context: { history } }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server}/300?key=value`, `http://${server}/300/1?key=value`, `http://${server}/300/2?key=value`, @@ -76,70 +77,70 @@ t.test('should follow redirection after a HTTP 300', async t => { } ) - t.equal(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) + t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`) }) -t.test('should follow redirection after a HTTP 301', async t => { - t.plan(3) +test('should follow redirection after a HTTP 301', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/301`, { method: 'POST', body: 'REQUEST', opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) + t.strictEqual(body.join(''), `POST /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) }) -t.test('should follow redirection after a HTTP 302', async t => { - t.plan(3) +test('should follow redirection after a HTTP 302', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/302`, { method: 'PUT', body: Buffer.from('REQUEST'), opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) + t.strictEqual(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`) }) -t.test('should follow redirection after a HTTP 303 changing method to GET', async t => { - t.plan(3) +test('should follow redirection after a HTTP 303 changing method to GET', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream(`http://${server}/303`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) }) - t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive`) + t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`) }) -t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { - t.plan(3) +test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/303`, @@ -165,21 +166,21 @@ t.test('should remove Host and request body related headers when following HTTP maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) }) -t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { - t.plan(3) +test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/303`, @@ -198,111 +199,111 @@ t.test('should remove Host and request body related headers when following HTTP maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) + t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`) }) -t.test('should follow redirection after a HTTP 307', async t => { - t.plan(3) +test('should follow redirection after a HTTP 307', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/307`, { method: 'DELETE', opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`) + t.strictEqual(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`) }) -t.test('should follow redirection after a HTTP 308', async t => { - t.plan(3) +test('should follow redirection after a HTTP 308', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/308`, { method: 'OPTIONS', opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`) + t.strictEqual(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`) }) -t.test('should ignore HTTP 3xx response bodies', async t => { - t.plan(4) +test('should ignore HTTP 3xx response bodies', async t => { + t = tspl(t, { plan: 4 }) const body = [] - const server = await startRedirectingWithBodyServer(t) + const server = await startRedirectingWithBodyServer() await stream( `http://${server}/`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque, context: { history } }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`]) + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`]) return createWritable(opaque) } ) - t.equal(body.join(''), 'FINAL') + t.strictEqual(body.join(''), 'FINAL') }) -t.test('should follow a redirect chain up to the allowed number of times', async t => { - t.plan(4) +test('should follow a redirect chain up to the allowed number of times', async t => { + t = tspl(t, { plan: 4 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}/300`, { opaque: body, maxRedirections: 2 }, ({ statusCode, headers, opaque, context: { history } }) => { - t.equal(statusCode, 300) - t.equal(headers.location, `http://${server}/300/3`) - t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`]) + t.strictEqual(statusCode, 300) + t.strictEqual(headers.location, `http://${server}/300/3`) + t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`]) return createWritable(opaque) } ) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) -t.test('should follow redirections when going cross origin', async t => { - t.plan(4) +test('should follow redirections when going cross origin', async t => { + t = tspl(t, { plan: 4 }) - const [server1, server2, server3] = await startRedirectingChainServers(t) + const [server1, server2, server3] = await startRedirectingChainServers() const body = [] await stream( `http://${server1}`, { method: 'POST', opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque, context: { history } }) => { - t.equal(statusCode, 200) - t.notOk(headers.location) - t.same(history.map(x => x.toString()), [ + t.strictEqual(statusCode, 200) + t.strictEqual(headers.location, undefined) + t.deepStrictEqual(history.map(x => x.toString()), [ `http://${server1}/`, `http://${server2}/`, `http://${server3}/`, @@ -315,16 +316,16 @@ t.test('should follow redirections when going cross origin', async t => { } ) - t.equal(body.join(''), 'POST') + t.strictEqual(body.join(''), 'POST') }) -t.test('when a Location response header is NOT present', async t => { +describe('when a Location response header is NOT present', async () => { const redirectCodes = [300, 301, 302, 303, 307, 308] - const server = await startRedirectingWithoutLocationServer(t) + const server = await startRedirectingWithoutLocationServer() for (const code of redirectCodes) { - t.test(`should return the original response after a HTTP ${code}`, async t => { - t.plan(3) + test(`should return the original response after a HTTP ${code}`, async t => { + t = tspl(t, { plan: 3 }) const body = [] @@ -332,23 +333,24 @@ t.test('when a Location response header is NOT present', async t => { `http://${server}/${code}`, { opaque: body, maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, code) - t.notOk(headers.location) + t.strictEqual(statusCode, code) + t.strictEqual(headers.location, undefined) return createWritable(opaque) } ) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) + await t.completed }) } }) -t.test('should not follow redirects when using Readable request bodies', async t => { - t.plan(3) +test('should not follow redirects when using Readable request bodies', async t => { + t = tspl(t, { plan: 3 }) const body = [] - const server = await startRedirectingServer(t) + const server = await startRedirectingServer() await stream( `http://${server}`, @@ -359,18 +361,18 @@ t.test('should not follow redirects when using Readable request bodies', async t maxRedirections: 10 }, ({ statusCode, headers, opaque }) => { - t.equal(statusCode, 302) - t.equal(headers.location, `http://${server}/302/1`) + t.strictEqual(statusCode, 302) + t.strictEqual(headers.location, `http://${server}/302/1`) return createWritable(opaque) } ) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) -t.test('should handle errors', async t => { - t.plan(2) +test('should handle errors', async t => { + t = tspl(t, { plan: 2 }) const body = [] @@ -382,16 +384,16 @@ t.test('should handle errors', async t => { throw new Error('Did not throw') } catch (error) { t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) } }) -t.test('removes authorization header on third party origin', async t => { - t.plan(1) +test('removes authorization header on third party origin', async t => { + t = tspl(t, { plan: 1 }) const body = [] - const [server1] = await startRedirectingWithAuthorization(t, 'secret') + const [server1] = await startRedirectingWithAuthorization('secret') await stream(`http://${server1}`, { maxRedirections: 10, opaque: body, @@ -400,15 +402,15 @@ t.test('removes authorization header on third party origin', async t => { } }, ({ statusCode, headers, opaque }) => createWritable(opaque)) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) -t.test('removes cookie header on third party origin', async t => { - t.plan(1) +test('removes cookie header on third party origin', async t => { + t = tspl(t, { plan: 1 }) const body = [] - const [server1] = await startRedirectingWithCookie(t, 'a=b') + const [server1] = await startRedirectingWithCookie('a=b') await stream(`http://${server1}`, { maxRedirections: 10, opaque: body, @@ -417,7 +419,5 @@ t.test('removes cookie header on third party origin', async t => { } }, ({ statusCode, headers, opaque }) => createWritable(opaque)) - t.equal(body.length, 0) + t.strictEqual(body.length, 0) }) - -t.teardown(() => process.exit()) diff --git a/test/redirect-upgrade.js b/test/redirect-upgrade.js index dbe584065df..5e331797379 100644 --- a/test/redirect-upgrade.js +++ b/test/redirect-upgrade.js @@ -1,13 +1,14 @@ 'use strict' -const t = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { upgrade } = require('..') const { startServer } = require('./utils/redirecting-servers') -t.test('should upgrade the connection when no redirects are present', async t => { - t.plan(2) +test('should upgrade the connection when no redirects are present', async t => { + t = tspl(t, { plan: 2 }) - const server = await startServer(t, (req, res) => { + const server = await startServer((req, res) => { if (req.url === '/') { res.statusCode = 301 res.setHeader('Location', `http://${server}/end`) @@ -29,6 +30,8 @@ t.test('should upgrade the connection when no redirects are present', async t => socket.end() - t.equal(headers.connection, 'upgrade') - t.equal(headers.upgrade, 'foo/1') + t.strictEqual(headers.connection, 'upgrade') + t.strictEqual(headers.upgrade, 'foo/1') + + await t.completed }) diff --git a/test/utils/redirecting-servers.js b/test/utils/redirecting-servers.js index 0125fc55748..011979c3e1e 100644 --- a/test/utils/redirecting-servers.js +++ b/test/utils/redirecting-servers.js @@ -1,6 +1,7 @@ 'use strict' const { createServer } = require('node:http') +const { after } = require('node:test') const isNode20 = process.version.startsWith('v20.') @@ -15,7 +16,7 @@ function close (server) { } } -function startServer (t, handler) { +function startServer (handler) { return new Promise(resolve => { const server = createServer(handler) @@ -23,12 +24,12 @@ function startServer (t, handler) { resolve(`localhost:${server.address().port}`) }) - t.teardown(close(server)) + after(close(server)) }) } -async function startRedirectingServer (t) { - const server = await startServer(t, (req, res) => { +async function startRedirectingServer () { + const server = await startServer((req, res) => { // Parse the path and normalize arguments let [code, redirections, query] = req.url .slice(1) @@ -90,8 +91,8 @@ async function startRedirectingServer (t) { return server } -async function startRedirectingWithBodyServer (t) { - const server = await startServer(t, (req, res) => { +async function startRedirectingWithBodyServer () { + const server = await startServer((req, res) => { if (req.url === '/') { res.statusCode = 301 res.setHeader('Connection', 'close') @@ -107,8 +108,8 @@ async function startRedirectingWithBodyServer (t) { return server } -function startRedirectingWithoutLocationServer (t) { - return startServer(t, (req, res) => { +function startRedirectingWithoutLocationServer () { + return startServer((req, res) => { // Parse the path and normalize arguments let [code] = req.url .slice(1) @@ -125,8 +126,8 @@ function startRedirectingWithoutLocationServer (t) { }) } -async function startRedirectingChainServers (t) { - const server1 = await startServer(t, (req, res) => { +async function startRedirectingChainServers () { + const server1 = await startServer((req, res) => { if (req.url === '/') { res.statusCode = 301 res.setHeader('Connection', 'close') @@ -139,7 +140,7 @@ async function startRedirectingChainServers (t) { res.end(req.method) }) - const server2 = await startServer(t, (req, res) => { + const server2 = await startServer((req, res) => { res.statusCode = 301 res.setHeader('Connection', 'close') @@ -152,7 +153,7 @@ async function startRedirectingChainServers (t) { res.end('') }) - const server3 = await startServer(t, (req, res) => { + const server3 = await startServer((req, res) => { res.statusCode = 301 res.setHeader('Connection', 'close') @@ -168,8 +169,8 @@ async function startRedirectingChainServers (t) { return [server1, server2, server3] } -async function startRedirectingWithAuthorization (t, authorization) { - const server1 = await startServer(t, (req, res) => { +async function startRedirectingWithAuthorization (authorization) { + const server1 = await startServer((req, res) => { if (req.headers.authorization !== authorization) { res.statusCode = 403 res.setHeader('Connection', 'close') @@ -184,15 +185,15 @@ async function startRedirectingWithAuthorization (t, authorization) { res.end('') }) - const server2 = await startServer(t, (req, res) => { + const server2 = await startServer((req, res) => { res.end(req.headers.authorization || '') }) return [server1, server2] } -async function startRedirectingWithCookie (t, cookie) { - const server1 = await startServer(t, (req, res) => { +async function startRedirectingWithCookie (cookie) { + const server1 = await startServer((req, res) => { if (req.headers.cookie !== cookie) { res.statusCode = 403 res.setHeader('Connection', 'close') @@ -207,7 +208,7 @@ async function startRedirectingWithCookie (t, cookie) { res.end('') }) - const server2 = await startServer(t, (req, res) => { + const server2 = await startServer((req, res) => { res.end(req.headers.cookie || '') }) @@ -215,7 +216,7 @@ async function startRedirectingWithCookie (t, cookie) { } async function startRedirectingWithRelativePath (t) { - const server = await startServer(t, (req, res) => { + const server = await startServer((req, res) => { res.setHeader('Connection', 'close') if (req.url === '/') { @@ -236,7 +237,7 @@ async function startRedirectingWithRelativePath (t) { } async function startRedirectingWithQueryParams (t) { - const server = await startServer(t, (req, res) => { + const server = await startServer((req, res) => { if (req.url === '/?param1=first') { res.statusCode = 301 res.setHeader('Connection', 'close') From 43286a995fa67b71188cc63f419583ea06867327 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:02:32 +0100 Subject: [PATCH 021/123] chore: migrate a batch of tests to node test runner (#2738) --- test/invalid-headers.js | 7 +- test/issue-2078.js | 13 ++- test/issue-803.js | 48 +++++---- test/issue-810.js | 178 +++++++++++++++++-------------- test/max-headers.js | 50 +++++---- test/no-strict-content-length.js | 105 ++++++++++-------- test/proxy.js | 27 ++--- test/readable.test.js | 22 ++-- test/request-crlf.js | 40 +++---- test/request-timeout2.js | 61 ++++++----- 10 files changed, 299 insertions(+), 252 deletions(-) diff --git a/test/invalid-headers.js b/test/invalid-headers.js index 0c011e97ac1..5d49fcbf23f 100644 --- a/test/invalid-headers.js +++ b/test/invalid-headers.js @@ -1,13 +1,14 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') test('invalid headers', (t) => { - t.plan(10) + t = tspl(t, { plan: 10 }) const client = new Client('http://localhost:3000') - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', diff --git a/test/issue-2078.js b/test/issue-2078.js index 335d7ab3820..40597919c24 100644 --- a/test/issue-2078.js +++ b/test/issue-2078.js @@ -1,24 +1,29 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { MockAgent, getGlobalDispatcher, setGlobalDispatcher, fetch } = require('..') test('MockPool.reply headers are an object, not an array - issue #2078', async (t) => { + t = tspl(t, { plan: 1 }) + const global = getGlobalDispatcher() const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost') - t.teardown(() => setGlobalDispatcher(global)) + after(() => setGlobalDispatcher(global)) setGlobalDispatcher(mockAgent) mockPool.intercept({ path: '/foo', method: 'GET' }).reply((options) => { - t.ok(!Array.isArray(options.headers)) + t.strictEqual(Array.isArray(options.headers), false) return { statusCode: 200 } }) - await t.resolves(fetch('http://localhost/foo')) + await fetch('http://localhost/foo') + + await t.completed }) diff --git a/test/issue-803.js b/test/issue-803.js index 35975103967..849bc1aba47 100644 --- a/test/issue-803.js +++ b/test/issue-803.js @@ -1,12 +1,14 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const EE = require('node:events') -test('https://github.com/nodejs/undici/issues/803', (t) => { - t.plan(2) +test('https://github.com/nodejs/undici/issues/803', async (t) => { + t = tspl(t, { plan: 2 }) const SIZE = 5900373096 @@ -23,25 +25,27 @@ test('https://github.com/nodejs/undici/issues/803', (t) => { res.end() }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - - let pos = 0 - data.body.on('data', (buf) => { - pos += buf.length - }) - data.body.on('end', () => { - t.equal(pos, SIZE) - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + + let pos = 0 + data.body.on('data', (buf) => { + pos += buf.length + }) + data.body.on('end', () => { + t.strictEqual(pos, SIZE) }) }) + await t.completed }) diff --git a/test/issue-810.js b/test/issue-810.js index e18656c5869..2a132cf30f3 100644 --- a/test/issue-810.js +++ b/test/issue-810.js @@ -1,11 +1,13 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client, errors } = require('..') const net = require('node:net') -test('https://github.com/mcollina/undici/issues/810', (t) => { - t.plan(3) +test('https://github.com/mcollina/undici/issues/810', async (t) => { + t = tspl(t, { plan: 3 }) let x = 0 const server = net.createServer(socket => { @@ -19,117 +21,127 @@ test('https://github.com/mcollina/undici/issues/810', (t) => { socket.write('\r\n') } }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - // t.fail() FIX: Should fail. - t.ok(true, 'pass') - }).on('error', err => ( - t.ok(err instanceof errors.HTTPParserError) - )) - }) - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + // t.fail() FIX: Should fail. + t.ok(true, 'pass') + }).on('error', err => ( t.ok(err instanceof errors.HTTPParserError) - }) + )) + }) + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ok(err instanceof errors.HTTPParserError) }) + await t.completed }) -test('https://github.com/mcollina/undici/issues/810 no pipelining', (t) => { - t.plan(2) +test('https://github.com/mcollina/undici/issues/810 no pipelining', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 1\r\n\r\n') socket.write('11111\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - // t.fail() FIX: Should fail. - t.ok(true, 'pass') - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + // t.fail() FIX: Should fail. + t.ok(true, 'pass') }) }) + await t.completed }) -test('https://github.com/mcollina/undici/issues/810 pipelining', (t) => { - t.plan(2) +test('https://github.com/mcollina/undici/issues/810 pipelining', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 1\r\n\r\n') socket.write('11111\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true }) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - // t.fail() FIX: Should fail. - t.ok(true, 'pass') - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + // t.fail() FIX: Should fail. + t.ok(true, 'pass') }) }) + await t.completed }) -test('https://github.com/mcollina/undici/issues/810 pipelining 2', (t) => { - t.plan(4) +test('https://github.com/mcollina/undici/issues/810 pipelining 2', async (t) => { + t = tspl(t, { plan: 4 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 1\r\n\r\n') socket.write('11111\r\n') }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true }) - t.teardown(client.destroy.bind(client)) - - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - data.body.resume().on('end', () => { - // t.fail() FIX: Should fail. - t.ok(true, 'pass') - }) - }) + after(() => server.close()) - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.equal(err.code, 'HPE_INVALID_CONSTANT') - t.ok(err instanceof errors.HTTPParserError) + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`, { pipelining: true }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + data.body.resume().on('end', () => { + // t.fail() FIX: Should fail. + t.ok(true, 'pass') }) }) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.strictEqual(err.code, 'HPE_INVALID_CONSTANT') + t.ok(err instanceof errors.HTTPParserError) + }) + await t.completed }) diff --git a/test/max-headers.js b/test/max-headers.js index a7946e81098..0f1422a60b4 100644 --- a/test/max-headers.js +++ b/test/max-headers.js @@ -1,11 +1,13 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') +const { once } = require('node:events') -test('handle a lot of headers', (t) => { - t.plan(3) +test('handle a lot of headers', async (t) => { + t = tspl(t, { plan: 3 }) const headers = {} for (let n = 0; n < 64; ++n) { @@ -16,26 +18,28 @@ test('handle a lot of headers', (t) => { res.writeHead(200, headers) res.end() }) - t.teardown(server.close.bind(server)) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => server.close()) + server.listen(0) - client.request({ - path: '/', - method: 'GET' - }, (err, data) => { - t.error(err) - const headers2 = {} - for (let n = 0; n < 64; ++n) { - headers2[n] = data.headers[n] - } - t.strictSame(headers2, headers) - data.body - .resume() - .on('end', () => { - t.ok(true, 'pass') - }) - }) + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.ifError(err) + const headers2 = {} + for (let n = 0; n < 64; ++n) { + headers2[n] = data.headers[n] + } + t.deepStrictEqual(headers2, headers) + data.body + .resume() + .on('end', () => { + t.ok(true, 'pass') + }) }) + await t.completed }) diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index b16b82bc38e..d74f4677d56 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -1,15 +1,14 @@ 'use strict' -const tap = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const sinon = require('sinon') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -tap.test('strictContentLength: false', (t) => { - t.plan(7) - +describe('strictContentLength: false', (t) => { const emitWarningStub = sinon.stub(process, 'emitWarning') function assertEmitWarningCalledAndReset () { @@ -17,23 +16,23 @@ tap.test('strictContentLength: false', (t) => { emitWarningStub.resetHistory() } - t.teardown(() => { + after(() => { emitWarningStub.restore() }) - t.test('request invalid content-length', (t) => { - t.plan(8) + test('request invalid content-length', async (t) => { + t = tspl(t, { plan: 8 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -44,7 +43,7 @@ tap.test('strictContentLength: false', (t) => { body: 'asd' }, (err, data) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) client.request({ @@ -56,7 +55,7 @@ tap.test('strictContentLength: false', (t) => { body: 'asdasdasdasdasdasda' }, (err, data) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) client.request({ @@ -68,7 +67,7 @@ tap.test('strictContentLength: false', (t) => { body: Buffer.alloc(9) }, (err, data) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) client.request({ @@ -80,7 +79,7 @@ tap.test('strictContentLength: false', (t) => { body: Buffer.alloc(11) }, (err, data) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) client.request({ @@ -90,7 +89,7 @@ tap.test('strictContentLength: false', (t) => { 'content-length': 10 } }, (err, data) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -100,7 +99,7 @@ tap.test('strictContentLength: false', (t) => { 'content-length': 0 } }, (err, data) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -116,7 +115,7 @@ tap.test('strictContentLength: false', (t) => { } }) }, (err, data) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -132,24 +131,26 @@ tap.test('strictContentLength: false', (t) => { } }) }, (err, data) => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) - t.test('request streaming content-length less than body size', (t) => { - t.plan(1) + test('request streaming content-length less than body size', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -167,24 +168,26 @@ tap.test('strictContentLength: false', (t) => { }) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) - t.test('request streaming content-length greater than body size', (t) => { - t.plan(1) + test('request streaming content-length greater than body size', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -202,24 +205,26 @@ tap.test('strictContentLength: false', (t) => { }) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) - t.test('request streaming data when content-length=0', (t) => { - t.plan(1) + test('request streaming data when content-length=0', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -237,24 +242,26 @@ tap.test('strictContentLength: false', (t) => { }) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) - t.test('request async iterating content-length less than body size', (t) => { - t.plan(1) + test('request async iterating content-length less than body size', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -272,24 +279,26 @@ tap.test('strictContentLength: false', (t) => { })) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) - t.test('request async iterator content-length greater than body size', (t) => { - t.plan(1) + test('request async iterator content-length greater than body size', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -307,24 +316,25 @@ tap.test('strictContentLength: false', (t) => { })) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + await t.completed }) - t.test('request async iterator data when content-length=0', (t) => { - t.plan(1) + test('request async iterator data when content-length=0', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { strictContentLength: false }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -342,8 +352,9 @@ tap.test('strictContentLength: false', (t) => { })) }, (err) => { assertEmitWarningCalledAndReset() - t.error(err) + t.ifError(err) }) }) + await t.completed }) }) diff --git a/test/proxy.js b/test/proxy.js index c910d7224d1..e57babc8bc5 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -1,12 +1,13 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { Client, Pool } = require('..') const { createServer } = require('node:http') const proxy = require('proxy') test('connect through proxy', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -15,7 +16,7 @@ test('connect through proxy', async (t) => { const proxyUrl = `http://localhost:${proxy.address().port}` server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') + t.strictEqual(req.url, '/hello?foo=bar') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -32,8 +33,8 @@ test('connect through proxy', async (t) => { for await (const chunk of response.body) { data += chunk } - t.equal(response.statusCode, 200) - t.same(JSON.parse(data), { hello: 'world' }) + t.strictEqual(response.statusCode, 200) + t.deepStrictEqual(JSON.parse(data), { hello: 'world' }) server.close() proxy.close() @@ -41,7 +42,7 @@ test('connect through proxy', async (t) => { }) test('connect through proxy with auth', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -54,7 +55,7 @@ test('connect through proxy with auth', async (t) => { } server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') + t.strictEqual(req.url, '/hello?foo=bar') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -74,8 +75,8 @@ test('connect through proxy with auth', async (t) => { for await (const chunk of response.body) { data += chunk } - t.equal(response.statusCode, 200) - t.same(JSON.parse(data), { hello: 'world' }) + t.strictEqual(response.statusCode, 200) + t.deepStrictEqual(JSON.parse(data), { hello: 'world' }) server.close() proxy.close() @@ -83,7 +84,7 @@ test('connect through proxy with auth', async (t) => { }) test('connect through proxy (with pool)', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -92,7 +93,7 @@ test('connect through proxy (with pool)', async (t) => { const proxyUrl = `http://localhost:${proxy.address().port}` server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') + t.strictEqual(req.url, '/hello?foo=bar') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -109,8 +110,8 @@ test('connect through proxy (with pool)', async (t) => { for await (const chunk of response.body) { data += chunk } - t.equal(response.statusCode, 200) - t.same(JSON.parse(data), { hello: 'world' }) + t.strictEqual(response.statusCode, 200) + t.deepStrictEqual(JSON.parse(data), { hello: 'world' }) server.close() proxy.close() diff --git a/test/readable.test.js b/test/readable.test.js index 535ecb66baa..3d6b5b1cea1 100644 --- a/test/readable.test.js +++ b/test/readable.test.js @@ -1,9 +1,12 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const Readable = require('../lib/api/readable') test('avoid body reordering', async function (t) { + t = tspl(t, { plan: 1 }) + function resume () { } function abort () { @@ -19,28 +22,25 @@ test('avoid body reordering', async function (t) { const text = await r.text() - t.equal(text, 'helloworld') + t.strictEqual(text, 'helloworld') }) test('destroy timing text', async function (t) { - t.plan(1) + t = tspl(t, { plan: 1 }) function resume () { } function abort () { } - const _err = new Error('kaboom') + const r = new Readable({ resume, abort }) - r.destroy(_err) - try { - await r.text() - } catch (err) { - t.same(err, _err) - } + r.destroy(new Error('kaboom')) + + t.rejects(r.text(), new Error('kaboom')) }) test('destroy timing promise', async function (t) { - t.plan(1) + t = tspl(t, { plan: 1 }) function resume () { } diff --git a/test/request-crlf.js b/test/request-crlf.js index be33f767a4d..4e6921e2fb0 100644 --- a/test/request-crlf.js +++ b/test/request-crlf.js @@ -1,11 +1,13 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') -const { test } = require('tap') +const { test, after } = require('node:test') const { request, errors } = require('..') +const { once } = require('node:events') -test('should validate content-type CRLF Injection', (t) => { - t.plan(2) +test('should validate content-type CRLF Injection', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { t.fail('should not receive any request') @@ -13,20 +15,22 @@ test('should validate content-type CRLF Injection', (t) => { res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, async () => { - try { - await request(`http://localhost:${server.address().port}`, { - method: 'GET', - headers: { - 'content-type': 'application/json\r\n\r\nGET /foo2 HTTP/1.1' - } - }) - t.fail('request should fail') - } catch (e) { - t.ok(e instanceof errors.InvalidArgumentError) - t.equal(e.message, 'invalid content-type header') - } - }) + server.listen(0) + + await once(server, 'listening') + try { + await request(`http://localhost:${server.address().port}`, { + method: 'GET', + headers: { + 'content-type': 'application/json\r\n\r\nGET /foo2 HTTP/1.1' + } + }) + t.fail('request should fail') + } catch (e) { + t.ok(e instanceof errors.InvalidArgumentError) + t.strictEqual(e.message, 'invalid content-type header') + } + await t.completed }) diff --git a/test/request-timeout2.js b/test/request-timeout2.js index ff4f1ddfebc..a294b2fb9c1 100644 --- a/test/request-timeout2.js +++ b/test/request-timeout2.js @@ -1,12 +1,14 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -test('request timeout with slow readable body', (t) => { - t.plan(1) +test('request timeout with slow readable body', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer(async (req, res) => { let str = '' @@ -15,34 +17,37 @@ test('request timeout with slow readable body', (t) => { } res.end(str) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) - t.teardown(client.close.bind(client)) + server.listen(0) - const body = new Readable({ - read () { - if (this._reading) { - return - } - this._reading = true + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) + after(() => client.close()) - this.push('asd') - setTimeout(() => { - this.push('asd') - this.push(null) - }, 2e3) + const body = new Readable({ + read () { + if (this._reading) { + return } - }) - client.request({ - path: '/', - method: 'POST', - headersTimeout: 1e3, - body - }, async (err, response) => { - t.error(err) - await response.body.dump() - }) + this._reading = true + + this.push('asd') + setTimeout(() => { + this.push('asd') + this.push(null) + }, 2e3) + } + }) + client.request({ + path: '/', + method: 'POST', + headersTimeout: 1e3, + body + }, async (err, response) => { + t.ifError(err) + await response.body.dump() }) + + await t.completed }) From 07fe3609d266ddc2309c303d9d87c001bb534791 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 12:08:16 +0100 Subject: [PATCH 022/123] chore: migrate a batch of tests to node test runner (#2741) --- test/content-length.js | 120 ++++++++++++++++++-------------- test/esm-wrapper.js | 2 +- test/fixed-queue.js | 21 +++--- test/promises.js | 121 ++++++++++++++++++-------------- test/proxy-agent.js | 121 ++++++++++++++++---------------- test/retry-handler.js | 129 ++++++++++++++++++++--------------- test/socket-back-pressure.js | 60 ++++++++-------- test/utils/esm-wrapper.mjs | 93 +++++++++++++------------ 8 files changed, 363 insertions(+), 304 deletions(-) diff --git a/test/content-length.js b/test/content-length.js index 6c124ed3530..847e2bca023 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -1,21 +1,22 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { maybeWrapStream, consts } = require('./utils/async-iterators') -test('request invalid content-length', (t) => { - t.plan(7) +test('request invalid content-length', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -94,19 +95,21 @@ test('request invalid content-length', (t) => { t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) + + await t.completed }) function invalidContentLength (bodyType) { - test(`request streaming ${bodyType} invalid content-length`, (t) => { - t.plan(4) + test(`request streaming ${bodyType} invalid content-length`, async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.once('disconnect', () => { t.ok(true, 'pass') @@ -151,6 +154,7 @@ function invalidContentLength (bodyType) { t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) + await t.completed }) } @@ -158,16 +162,16 @@ invalidContentLength(consts.STREAM) invalidContentLength(consts.ASYNC_ITERATOR) function zeroContentLength (bodyType) { - test(`request ${bodyType} streaming data when content-length=0`, (t) => { - t.plan(1) + test(`request ${bodyType} streaming data when content-length=0`, async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -187,22 +191,23 @@ function zeroContentLength (bodyType) { t.ok(err instanceof errors.RequestContentLengthMismatchError) }) }) + await t.completed }) } zeroContentLength(consts.STREAM) zeroContentLength(consts.ASYNC_ITERATOR) -test('request streaming no body data when content-length=0', (t) => { - t.plan(2) +test('request streaming no body data when content-length=0', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -211,7 +216,7 @@ test('request streaming no body data when content-length=0', (t) => { 'content-length': 0 } }, (err, data) => { - t.error(err) + t.ifError(err) data.body .on('data', () => { t.fail() @@ -221,10 +226,12 @@ test('request streaming no body data when content-length=0', (t) => { }) }) }) + + await t.completed }) -test('response invalid content length with close', (t) => { - t.plan(3) +test('response invalid content length with close', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.writeHead(200, { @@ -232,42 +239,44 @@ test('response invalid content length with close', (t) => { }) res.end('123') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 }) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.on('disconnect', (origin, client, err) => { - t.equal(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH') + t.strictEqual(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH') }) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .on('end', () => { t.fail() }) .on('error', (err) => { - t.equal(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH') + t.strictEqual(err.code, 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH') }) .resume() }) }) + + await t.completed }) -test('request streaming with Readable.from(buf)', (t) => { +test('request streaming with Readable.from(buf)', async (t) => { const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -275,32 +284,34 @@ test('request streaming with Readable.from(buf)', (t) => { body: Readable.from(Buffer.from('hello')) }, (err, data) => { const chunks = [] - t.error(err) + t.ifError(err) data.body .on('data', (chunk) => { chunks.push(chunk) }) .on('end', () => { - t.equal(Buffer.concat(chunks).toString(), 'hello') + t.strictEqual(Buffer.concat(chunks).toString(), 'hello') t.ok(true, 'pass') t.end() }) }) }) + + await t.completed }) -test('request DELETE, content-length=0, with body', (t) => { - t.plan(5) +test('request DELETE, content-length=0, with body', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { res.end() }) server.on('request', (req, res) => { - t.equal(req.headers['content-length'], undefined) + t.strictEqual(req.headers['content-length'], undefined) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -325,38 +336,41 @@ test('request DELETE, content-length=0, with body', (t) => { 'content-length': 0 } }, (err, resp) => { - t.equal(resp.headers['content-length'], '0') - t.error(err) + t.strictEqual(resp.headers['content-length'], '0') + t.ifError(err) }) client.on('disconnect', () => { t.ok(true, 'pass') }) }) + + await t.completed }) -test('content-length shouldSendContentLength=false', (t) => { - t.plan(15) +test('content-length shouldSendContentLength=false', async (t) => { + t = tspl(t, { plan: 15 }) + const server = createServer((req, res) => { res.end() }) server.on('request', (req, res) => { switch (req.url) { case '/put0': - t.equal(req.headers['content-length'], '0') + t.strictEqual(req.headers['content-length'], '0') break case '/head': - t.equal(req.headers['content-length'], undefined) + t.strictEqual(req.headers['content-length'], undefined) break case '/get': - t.equal(req.headers['content-length'], undefined) + t.strictEqual(req.headers['content-length'], undefined) break } }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.close()) client.request({ path: '/put0', @@ -365,8 +379,8 @@ test('content-length shouldSendContentLength=false', (t) => { 'content-length': 0 } }, (err, resp) => { - t.equal(resp.headers['content-length'], '0') - t.error(err) + t.strictEqual(resp.headers['content-length'], '0') + t.ifError(err) }) client.request({ @@ -376,8 +390,8 @@ test('content-length shouldSendContentLength=false', (t) => { 'content-length': 10 } }, (err, resp) => { - t.equal(resp.headers['content-length'], undefined) - t.error(err) + t.strictEqual(resp.headers['content-length'], undefined) + t.ifError(err) }) client.request({ @@ -387,7 +401,7 @@ test('content-length shouldSendContentLength=false', (t) => { 'content-length': 0 } }, (err) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -403,7 +417,7 @@ test('content-length shouldSendContentLength=false', (t) => { } }) }, (err) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -419,7 +433,7 @@ test('content-length shouldSendContentLength=false', (t) => { } }) }, (err) => { - t.error(err) + t.ifError(err) }) client.request({ @@ -435,11 +449,13 @@ test('content-length shouldSendContentLength=false', (t) => { } }) }, (err) => { - t.error(err) + t.ifError(err) }) client.on('disconnect', () => { t.ok(true, 'pass') }) }) + + await t.completed }) diff --git a/test/esm-wrapper.js b/test/esm-wrapper.js index 8adb3278719..241fb3ea876 100644 --- a/test/esm-wrapper.js +++ b/test/esm-wrapper.js @@ -5,7 +5,7 @@ await import('./utils/esm-wrapper.mjs') } catch (e) { if (e.message === 'Not supported') { - require('tap') // shows skipped + require('node:test') // shows skipped return } console.error(e.stack) diff --git a/test/fixed-queue.js b/test/fixed-queue.js index 812f421b280..cb878716e44 100644 --- a/test/fixed-queue.js +++ b/test/fixed-queue.js @@ -1,23 +1,24 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const FixedQueue = require('../lib/node/fixed-queue') test('fixed queue 1', (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const queue = new FixedQueue() - t.equal(queue.head, queue.tail) + t.strictEqual(queue.head, queue.tail) t.ok(queue.isEmpty()) queue.push('a') t.ok(!queue.isEmpty()) - t.equal(queue.shift(), 'a') - t.equal(queue.shift(), null) + t.strictEqual(queue.shift(), 'a') + t.strictEqual(queue.shift(), null) }) test('fixed queue 2', (t) => { - t.plan(7 + 2047) + t = tspl(t, { plan: 7 + 2047 }) const queue = new FixedQueue() for (let i = 0; i < 2047; i++) { @@ -27,12 +28,12 @@ test('fixed queue 2', (t) => { queue.push('a') t.ok(!queue.head.isFull()) - t.not(queue.head, queue.tail) + t.notEqual(queue.head, queue.tail) for (let i = 0; i < 2047; i++) { - t.equal(queue.shift(), 'a') + t.strictEqual(queue.shift(), 'a') } - t.equal(queue.head, queue.tail) + t.strictEqual(queue.head, queue.tail) t.ok(!queue.isEmpty()) - t.equal(queue.shift(), 'a') + t.strictEqual(queue.shift(), 'a') t.ok(queue.isEmpty()) }) diff --git a/test/promises.js b/test/promises.js index 4188a125bd9..5d47adc3640 100644 --- a/test/promises.js +++ b/test/promises.js @@ -1,47 +1,50 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, Pool } = require('..') const { createServer } = require('node:http') const { readFileSync, createReadStream } = require('node:fs') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -test('basic get, async await support', (t) => { - t.plan(5) +test('basic get, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) function postServer (t, expected) { return function (req, res) { - t.equal(req.url, '/') - t.equal(req.method, 'POST') + t.strictEqual(req.url, '/') + t.strictEqual(req.method, 'POST') req.setEncoding('utf8') let data = '' @@ -49,79 +52,83 @@ function postServer (t, expected) { req.on('data', function (d) { data += d }) req.on('end', () => { - t.equal(data, expected) + t.strictEqual(data, expected) res.end('hello') }) } } -test('basic POST with string, async await support', (t) => { - t.plan(5) +test('basic POST with string, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) -test('basic POST with Buffer, async await support', (t) => { - t.plan(5) +test('basic POST with Buffer, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const expected = readFileSync(__filename) const server = createServer(postServer(t, expected.toString())) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, body } = await client.request({ path: '/', method: 'POST', body: expected }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) -test('basic POST with stream, async await support', (t) => { - t.plan(5) +test('basic POST with stream, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, body } = await client.request({ @@ -132,31 +139,33 @@ test('basic POST with stream, async await support', (t) => { }, body: createReadStream(__filename) }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) -test('basic POST with async-iterator, async await support', (t) => { - t.plan(5) +test('basic POST with async-iterator, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, body } = await client.request({ @@ -167,23 +176,25 @@ test('basic POST with async-iterator, async await support', (t) => { }, body: wrapWithAsyncIterable(createReadStream(__filename)) }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) -test('20 times GET with pipelining 10, async await support', (t) => { +test('20 times GET with pipelining 10, async await support', async (t) => { const num = 20 - t.plan(2 * num + 1) + t = tspl(t, { plan: 2 * num + 1 }) const sleep = ms => new Promise((resolve, reject) => { setTimeout(resolve, ms) @@ -197,7 +208,7 @@ test('20 times GET with pipelining 10, async await support', (t) => { countGreaterThanOne = countGreaterThanOne || count > 1 res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) // needed to check for a warning on the maxListeners on the socket function onWarning (warning) { @@ -206,7 +217,7 @@ test('20 times GET with pipelining 10, async await support', (t) => { } } process.on('warning', onWarning) - t.teardown(() => { + after(() => { process.removeListener('warning', onWarning) }) @@ -214,7 +225,7 @@ test('20 times GET with pipelining 10, async await support', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) for (let i = 0; i < num; i++) { makeRequest(i) @@ -228,18 +239,20 @@ test('20 times GET with pipelining 10, async await support', (t) => { } } }) + + await t.completed }) async function makeRequestAndExpectUrl (client, i, t) { try { const { statusCode, body } = await client.request({ path: '/' + i, method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('/' + i, Buffer.concat(bufs).toString('utf8')) + t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) @@ -247,34 +260,36 @@ async function makeRequestAndExpectUrl (client, i, t) { return true } -test('pool, async await support', (t) => { - t.plan(5) +test('pool, async await support', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } catch (err) { t.fail(err) } }) + + await t.completed }) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 70e76756a54..87129d01472 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -1,6 +1,7 @@ 'use strict' -const { test, teardown } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..') const { InvalidArgumentError } = require('../lib/core/errors') const { readFileSync } = require('node:fs') @@ -12,13 +13,13 @@ const https = require('node:https') const proxy = require('proxy') test('should throw error when no uri is provided', (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) t.throws(() => new ProxyAgent(), InvalidArgumentError) t.throws(() => new ProxyAgent({}), InvalidArgumentError) }) test('using auth in combination with token should throw', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) t.throws(() => new ProxyAgent({ auth: 'foo', token: 'Bearer bar', @@ -29,13 +30,13 @@ test('using auth in combination with token should throw', (t) => { }) test('should accept string and object as options', (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) t.doesNotThrow(() => new ProxyAgent('http://example.com')) t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' })) }) test('use proxy-agent to connect through proxy', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = await buildServer() const proxy = await buildProxy() delete proxy.authenticate @@ -50,8 +51,8 @@ test('use proxy-agent to connect through proxy', async (t) => { }) server.on('request', (req, res) => { - t.equal(req.url, '/') - t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + t.strictEqual(req.url, '/') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -63,9 +64,9 @@ test('use proxy-agent to connect through proxy', async (t) => { } = await request(serverUrl, { dispatcher: proxyAgent }) const json = await body.json() - t.equal(statusCode, 200) - t.same(json, { hello: 'world' }) - t.equal(headers.connection, 'keep-alive', 'should remain the connection open') + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') server.close() proxy.close() @@ -73,7 +74,7 @@ test('use proxy-agent to connect through proxy', async (t) => { }) test('use proxy agent to connect through proxy using Pool', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() let resolveFirstConnect @@ -104,15 +105,15 @@ test('use proxy agent to connect through proxy using Pool', async (t) => { const proxyAgent = new ProxyAgent({ auth: Buffer.from('user:pass').toString('base64'), uri: proxyUrl, clientFactory }) const firstRequest = request(`${serverUrl}`, { dispatcher: proxyAgent }) const secondRequest = await request(`${serverUrl}`, { dispatcher: proxyAgent }) - t.equal((await firstRequest).statusCode, 200) - t.equal(secondRequest.statusCode, 200) + t.strictEqual((await firstRequest).statusCode, 200) + t.strictEqual(secondRequest.statusCode, 200) server.close() proxy.close() proxyAgent.close() }) test('use proxy-agent to connect through proxy using path with params', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const server = await buildServer() const proxy = await buildProxy() @@ -125,8 +126,8 @@ test('use proxy-agent to connect through proxy using path with params', async (t t.ok(true, 'should call proxy') }) server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') - t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + t.strictEqual(req.url, '/hello?foo=bar') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -138,9 +139,9 @@ test('use proxy-agent to connect through proxy using path with params', async (t } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent }) const json = await body.json() - t.equal(statusCode, 200) - t.same(json, { hello: 'world' }) - t.equal(headers.connection, 'keep-alive', 'should remain the connection open') + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') server.close() proxy.close() @@ -148,7 +149,7 @@ test('use proxy-agent to connect through proxy using path with params', async (t }) test('use proxy-agent with auth', async (t) => { - t.plan(7) + t = tspl(t, { plan: 7 }) const server = await buildServer() const proxy = await buildProxy() @@ -169,8 +170,8 @@ test('use proxy-agent with auth', async (t) => { }) server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') - t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + t.strictEqual(req.url, '/hello?foo=bar') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -182,9 +183,9 @@ test('use proxy-agent with auth', async (t) => { } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent }) const json = await body.json() - t.equal(statusCode, 200) - t.same(json, { hello: 'world' }) - t.equal(headers.connection, 'keep-alive', 'should remain the connection open') + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') server.close() proxy.close() @@ -192,7 +193,7 @@ test('use proxy-agent with auth', async (t) => { }) test('use proxy-agent with token', async (t) => { - t.plan(7) + t = tspl(t, { plan: 7 }) const server = await buildServer() const proxy = await buildProxy() @@ -213,8 +214,8 @@ test('use proxy-agent with token', async (t) => { }) server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') - t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + t.strictEqual(req.url, '/hello?foo=bar') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -226,9 +227,9 @@ test('use proxy-agent with token', async (t) => { } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent }) const json = await body.json() - t.equal(statusCode, 200) - t.same(json, { hello: 'world' }) - t.equal(headers.connection, 'keep-alive', 'should remain the connection open') + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') server.close() proxy.close() @@ -236,7 +237,7 @@ test('use proxy-agent with token', async (t) => { }) test('use proxy-agent with custom headers', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const server = await buildServer() const proxy = await buildProxy() @@ -250,11 +251,11 @@ test('use proxy-agent with custom headers', async (t) => { }) proxy.on('connect', (req) => { - t.equal(req.headers['user-agent'], 'Foobar/1.0.0') + t.strictEqual(req.headers['user-agent'], 'Foobar/1.0.0') }) server.on('request', (req, res) => { - t.equal(req.headers['user-agent'], 'BarBaz/1.0.0') + t.strictEqual(req.headers['user-agent'], 'BarBaz/1.0.0') res.end() }) @@ -269,7 +270,7 @@ test('use proxy-agent with custom headers', async (t) => { }) test('sending proxy-authorization in request headers should throw', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -326,7 +327,7 @@ test('sending proxy-authorization in request headers should throw', async (t) => }) test('use proxy-agent with setGlobalDispatcher', async (t) => { - t.plan(6) + t = tspl(t, { plan: 6 }) const defaultDispatcher = getGlobalDispatcher() const server = await buildServer() @@ -338,14 +339,14 @@ test('use proxy-agent with setGlobalDispatcher', async (t) => { const parsedOrigin = new URL(serverUrl) setGlobalDispatcher(proxyAgent) - t.teardown(() => setGlobalDispatcher(defaultDispatcher)) + after(() => setGlobalDispatcher(defaultDispatcher)) proxy.on('connect', () => { t.ok(true, 'should call proxy') }) server.on('request', (req, res) => { - t.equal(req.url, '/hello?foo=bar') - t.equal(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + t.strictEqual(req.url, '/hello?foo=bar') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ hello: 'world' })) }) @@ -357,9 +358,9 @@ test('use proxy-agent with setGlobalDispatcher', async (t) => { } = await request(serverUrl + '/hello?foo=bar') const json = await body.json() - t.equal(statusCode, 200) - t.same(json, { hello: 'world' }) - t.equal(headers.connection, 'keep-alive', 'should remain the connection open') + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') server.close() proxy.close() @@ -367,7 +368,7 @@ test('use proxy-agent with setGlobalDispatcher', async (t) => { }) test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const defaultDispatcher = getGlobalDispatcher() const server = await buildServer() @@ -379,7 +380,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async const proxyAgent = new ProxyAgent(proxyUrl) setGlobalDispatcher(proxyAgent) - t.teardown(() => setGlobalDispatcher(defaultDispatcher)) + after(() => setGlobalDispatcher(defaultDispatcher)) const expectedHeaders = { host: `localhost:${server.address().port}`, @@ -398,11 +399,11 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async } proxy.on('connect', (req, res) => { - t.same(req.headers, expectedProxyHeaders) + t.deepStrictEqual(req.headers, expectedProxyHeaders) }) server.on('request', (req, res) => { - t.same(req.headers, expectedHeaders) + t.deepStrictEqual(req.headers, expectedHeaders) res.end('goodbye') }) @@ -417,6 +418,8 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async }) test('should throw when proxy does not return 200', async (t) => { + t = tspl(t, { plan: 2 }) + const server = await buildServer() const proxy = await buildProxy() @@ -439,10 +442,11 @@ test('should throw when proxy does not return 200', async (t) => { server.close() proxy.close() proxyAgent.close() - t.end() + await t.completed }) test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => { + t = tspl(t, { plan: 1 }) const server = await buildServer() const proxy = await buildProxy() @@ -457,17 +461,18 @@ test('pass ProxyAgent proxy status code error when using fetch - #2161', async ( try { await fetch(serverUrl, { dispatcher: proxyAgent }) } catch (e) { - t.hasProp(e, 'cause') + t.ok('cause' in e) } server.close() proxy.close() proxyAgent.close() - t.end() + + await t.completed }) test('Proxy via HTTP to HTTPS endpoint', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const server = await buildSSLServer() const proxy = await buildProxy() @@ -509,7 +514,7 @@ test('Proxy via HTTP to HTTPS endpoint', async (t) => { const data = await request(serverUrl, { dispatcher: proxyAgent }) const json = await data.body.json() - t.strictSame(json, { + t.deepStrictEqual(json, { host: `localhost:${server.address().port}`, connection: 'keep-alive' }) @@ -520,7 +525,7 @@ test('Proxy via HTTP to HTTPS endpoint', async (t) => { }) test('Proxy via HTTPS to HTTPS endpoint', async (t) => { - t.plan(5) + t = tspl(t, { plan: 5 }) const server = await buildSSLServer() const proxy = await buildSSLProxy() @@ -570,7 +575,7 @@ test('Proxy via HTTPS to HTTPS endpoint', async (t) => { const data = await request(serverUrl, { dispatcher: proxyAgent }) const json = await data.body.json() - t.strictSame(json, { + t.deepStrictEqual(json, { host: `localhost:${server.address().port}`, connection: 'keep-alive' }) @@ -581,7 +586,7 @@ test('Proxy via HTTPS to HTTPS endpoint', async (t) => { }) test('Proxy via HTTPS to HTTP endpoint', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildSSLProxy() @@ -619,7 +624,7 @@ test('Proxy via HTTPS to HTTP endpoint', async (t) => { const data = await request(serverUrl, { dispatcher: proxyAgent }) const json = await data.body.json() - t.strictSame(json, { + t.deepStrictEqual(json, { host: `localhost:${server.address().port}`, connection: 'keep-alive' }) @@ -630,7 +635,7 @@ test('Proxy via HTTPS to HTTP endpoint', async (t) => { }) test('Proxy via HTTP to HTTP endpoint', async (t) => { - t.plan(3) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -661,7 +666,7 @@ test('Proxy via HTTP to HTTP endpoint', async (t) => { const data = await request(serverUrl, { dispatcher: proxyAgent }) const json = await data.body.json() - t.strictSame(json, { + t.deepStrictEqual(json, { host: `localhost:${server.address().port}`, connection: 'keep-alive' }) @@ -715,5 +720,3 @@ function buildSSLProxy () { server.listen(0, () => resolve(server)) }) } - -teardown(() => process.exit()) diff --git a/test/retry-handler.js b/test/retry-handler.js index b4000606f62..f6b83cd34dd 100644 --- a/test/retry-handler.js +++ b/test/retry-handler.js @@ -1,13 +1,16 @@ 'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createServer } = require('node:http') const { once } = require('node:events') -const tap = require('tap') - const { RetryHandler, Client } = require('..') const { RequestHandler } = require('../lib/api/api-request') -tap.test('Should retry status code', t => { +test('Should retry status code', async t => { + t = tspl(t, { plan: 4 }) + let counter = 0 const chunks = [] const server = createServer() @@ -34,8 +37,6 @@ tap.test('Should retry status code', t => { } } - t.plan(4) - server.on('request', (req, res) => { switch (counter) { case 0: @@ -66,7 +67,7 @@ tap.test('Should retry status code', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 200) + t.strictEqual(status, 200) return true }, onData (chunk) { @@ -74,8 +75,8 @@ tap.test('Should retry status code', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') - t.equal(counter, 2) + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.strictEqual(counter, 2) }, onError () { t.fail() @@ -83,7 +84,7 @@ tap.test('Should retry status code', t => { } }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -101,9 +102,13 @@ tap.test('Should retry status code', t => { handler ) }) + + await t.completed }) -tap.test('Should use retry-after header for retries', t => { +test('Should use retry-after header for retries', async t => { + t = tspl(t, { plan: 4 }) + let counter = 0 const chunks = [] const server = createServer() @@ -116,8 +121,6 @@ tap.test('Should use retry-after header for retries', t => { } } - t.plan(4) - server.on('request', (req, res) => { switch (counter) { case 0: @@ -151,7 +154,7 @@ tap.test('Should use retry-after header for retries', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 200) + t.strictEqual(status, 200) return true }, onData (chunk) { @@ -159,15 +162,15 @@ tap.test('Should use retry-after header for retries', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!') }, onError (err) { - t.error(err) + t.ifError(err) } } }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -185,9 +188,13 @@ tap.test('Should use retry-after header for retries', t => { handler ) }) + + await t.completed }) -tap.test('Should use retry-after header for retries (date)', t => { +test('Should use retry-after header for retries (date)', async t => { + t = tspl(t, { plan: 4 }) + let counter = 0 const chunks = [] const server = createServer() @@ -200,8 +207,6 @@ tap.test('Should use retry-after header for retries (date)', t => { } } - t.plan(4) - server.on('request', (req, res) => { switch (counter) { case 0: @@ -237,7 +242,7 @@ tap.test('Should use retry-after header for retries (date)', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 200) + t.strictEqual(status, 200) return true }, onData (chunk) { @@ -245,15 +250,15 @@ tap.test('Should use retry-after header for retries (date)', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!') }, onError (err) { - t.error(err) + t.ifError(err) } } }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -271,9 +276,13 @@ tap.test('Should use retry-after header for retries (date)', t => { handler ) }) + + await t.completed }) -tap.test('Should retry with defaults', t => { +test('Should retry with defaults', async t => { + t = tspl(t, { plan: 3 }) + let counter = 0 const chunks = [] const server = createServer() @@ -306,8 +315,6 @@ tap.test('Should retry with defaults', t => { } }) - t.plan(3) - server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) const handler = new RetryHandler(dispatchOptions, { @@ -320,7 +327,7 @@ tap.test('Should retry with defaults', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 200) + t.strictEqual(status, 200) return true }, onData (chunk) { @@ -328,15 +335,15 @@ tap.test('Should retry with defaults', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!') }, onError (err) { - t.error(err) + t.ifError(err) } } }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -354,9 +361,13 @@ tap.test('Should retry with defaults', t => { handler ) }) + + await t.completed }) -tap.test('Should handle 206 partial content', t => { +test('Should handle 206 partial content', async t => { + t = tspl(t, { plan: 8 }) + const chunks = [] let counter = 0 @@ -371,7 +382,7 @@ tap.test('Should handle 206 partial content', t => { res.destroy() }, 1e2) } else if (x === 1) { - t.same(req.headers.range, 'bytes=3-') + t.deepStrictEqual(req.headers.range, 'bytes=3-') res.setHeader('content-range', 'bytes 3-6/6') res.setHeader('etag', 'asd') res.statusCode = 206 @@ -401,8 +412,6 @@ tap.test('Should handle 206 partial content', t => { } } - t.plan(8) - server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) const handler = new RetryHandler(dispatchOptions, { @@ -420,7 +429,7 @@ tap.test('Should handle 206 partial content', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 200) + t.strictEqual(status, 200) return true }, onData (chunk) { @@ -428,8 +437,8 @@ tap.test('Should handle 206 partial content', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'abcdef') - t.equal(counter, 1) + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.strictEqual(counter, 1) }, onError () { t.fail() @@ -448,16 +457,20 @@ tap.test('Should handle 206 partial content', t => { handler ) - t.teardown(async () => { + after(async () => { await client.close() server.close() await once(server, 'close') }) }) + + await t.completed }) -tap.test('Should handle 206 partial content - bad-etag', t => { +test('Should handle 206 partial content - bad-etag', async t => { + t = tspl(t, { plan: 6 }) + const chunks = [] // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 @@ -471,7 +484,7 @@ tap.test('Should handle 206 partial content - bad-etag', t => { res.destroy() }, 1e2) } else if (x === 1) { - t.same(req.headers.range, 'bytes=3-') + t.deepStrictEqual(req.headers.range, 'bytes=3-') res.setHeader('content-range', 'bytes 3-6/6') res.setHeader('etag', 'erwsd') res.statusCode = 206 @@ -488,8 +501,6 @@ tap.test('Should handle 206 partial content - bad-etag', t => { } } - t.plan(6) - server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) const handler = new RetryHandler( @@ -514,11 +525,11 @@ tap.test('Should handle 206 partial content - bad-etag', t => { return true }, onComplete () { - t.error('should not complete') + t.ifError('should not complete') }, onError (err) { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'abc') - t.equal(err.code, 'UND_ERR_REQ_RETRY') + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc') + t.strictEqual(err.code, 'UND_ERR_REQ_RETRY') } } } @@ -535,16 +546,18 @@ tap.test('Should handle 206 partial content - bad-etag', t => { handler ) - t.teardown(async () => { + after(async () => { await client.close() server.close() await once(server, 'close') }) }) + + await t.completed }) -tap.test('retrying a request with a body', t => { +test('retrying a request with a body', async t => { let counter = 0 const server = createServer() const dispatchOptions = { @@ -571,7 +584,7 @@ tap.test('retrying a request with a body', t => { body: JSON.stringify({ hello: 'world' }) } - t.plan(1) + t = tspl(t, { plan: 1 }) server.on('request', (req, res) => { switch (counter) { @@ -596,11 +609,11 @@ tap.test('retrying a request with a body', t => { const handler = new RetryHandler(dispatchOptions, { dispatch: client.dispatch.bind(client), handler: new RequestHandler(dispatchOptions, (err, data) => { - t.error(err) + t.ifError(err) }) }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -619,17 +632,19 @@ tap.test('retrying a request with a body', t => { handler ) }) + + await t.completed }) -tap.test('should not error if request is not meant to be retried', t => { +test('should not error if request is not meant to be retried', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer() server.on('request', (req, res) => { res.writeHead(400) res.end('Bad request') }) - t.plan(3) - const dispatchOptions = { retryOptions: { method: 'GET', @@ -653,7 +668,7 @@ tap.test('should not error if request is not meant to be retried', t => { t.ok(true, 'pass') }, onHeaders (status, _rawHeaders, resume, _statusMessage) { - t.equal(status, 400) + t.strictEqual(status, 400) return true }, onData (chunk) { @@ -661,7 +676,7 @@ tap.test('should not error if request is not meant to be retried', t => { return true }, onComplete () { - t.equal(Buffer.concat(chunks).toString('utf-8'), 'Bad request') + t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'Bad request') }, onError (err) { console.log({ err }) @@ -670,7 +685,7 @@ tap.test('should not error if request is not meant to be retried', t => { } }) - t.teardown(async () => { + after(async () => { await client.close() server.close() @@ -688,4 +703,6 @@ tap.test('should not error if request is not meant to be retried', t => { handler ) }) + + await t.completed }) diff --git a/test/socket-back-pressure.js b/test/socket-back-pressure.js index bdc2c74346f..043e815ed9a 100644 --- a/test/socket-back-pressure.js +++ b/test/socket-back-pressure.js @@ -1,12 +1,14 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -const { test } = require('tap') +const { test, after } = require('node:test') -test('socket back-pressure', (t) => { - t.plan(3) +test('socket back-pressure', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer() let bytesWritten = 0 @@ -25,30 +27,32 @@ test('socket back-pressure', (t) => { server.on('request', (req, res) => { src.pipe(res) }) - t.teardown(server.close.bind(server)) - - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`, { - pipelining: 1 - }) - t.teardown(client.destroy.bind(client)) - - client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => { - t.error(err) - data.body - .resume() - .once('data', () => { - data.body.pause() - // TODO: Try to avoid timeout. - setTimeout(() => { - t.ok(data.body._readableState.length < bytesWritten - data.body._readableState.highWaterMark) - src.push(null) - data.body.resume() - }, 1e3) - }) - .on('end', () => { - t.ok(true, 'pass') - }) - }) + after(() => server.close()) + + server.listen(0) + + await once(server, 'listening') + const client = new Client(`http://localhost:${server.address().port}`, { + pipelining: 1 + }) + after(() => client.close()) + + client.request({ path: '/', method: 'GET', opaque: 'asd' }, (err, data) => { + t.ifError(err) + data.body + .resume() + .once('data', () => { + data.body.pause() + // TODO: Try to avoid timeout. + setTimeout(() => { + t.ok(data.body._readableState.length < bytesWritten - data.body._readableState.highWaterMark) + src.push(null) + data.body.resume() + }, 1e3) + }) + .on('end', () => { + t.ok(true, 'pass') + }) }) + await t.completed }) diff --git a/test/utils/esm-wrapper.mjs b/test/utils/esm-wrapper.mjs index 0e8ac9ffb7a..104190c1afe 100644 --- a/test/utils/esm-wrapper.mjs +++ b/test/utils/esm-wrapper.mjs @@ -1,5 +1,7 @@ +import { tspl } from '@matteo.collina/tspl' import { createServer } from 'http' -import tap from 'tap' +import { test, after } from 'node:test' +import { once } from 'node:events' import { Agent, Client, @@ -14,55 +16,56 @@ import { stream } from '../../index.js' -const { test } = tap - -test('imported Client works with basic GET', (t) => { - t.plan(10) +test('imported Client works with basic GET', async (t) => { + t = tspl(t, { plan: 10 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(undefined, req.headers.foo) - t.equal('bar', req.headers.bar) - t.equal(undefined, req.headers['content-length']) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(undefined, req.headers.foo) + t.strictEqual('bar', req.headers.bar) + t.strictEqual(undefined, req.headers['content-length']) res.setHeader('Content-Type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const reqHeaders = { foo: undefined, bar: 'bar' } - server.listen(0, () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) - - client.request({ - path: '/', - method: 'GET', - headers: reqHeaders - }, (err, data) => { - t.error(err) - const { statusCode, headers, body } = data - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') - const bufs = [] - body.on('data', (buf) => { - bufs.push(buf) - }) - body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) - }) + server.listen(0) + + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET', + headers: reqHeaders + }, (err, data) => { + t.ifError(err) + const { statusCode, headers, body } = data + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') + const bufs = [] + body.on('data', (buf) => { + bufs.push(buf) + }) + body.on('end', () => { + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) + await t.completed }) test('imported errors work with request args validation', (t) => { - t.plan(2) + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:5000') @@ -78,7 +81,7 @@ test('imported errors work with request args validation', (t) => { }) test('imported errors work with request args validation promise', (t) => { - t.plan(1) + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:5000') @@ -88,15 +91,15 @@ test('imported errors work with request args validation promise', (t) => { }) test('named exports', (t) => { - t.equal(typeof Client, 'function') - t.equal(typeof Pool, 'function') - t.equal(typeof Agent, 'function') - t.equal(typeof request, 'function') - t.equal(typeof stream, 'function') - t.equal(typeof pipeline, 'function') - t.equal(typeof connect, 'function') - t.equal(typeof upgrade, 'function') - t.equal(typeof setGlobalDispatcher, 'function') - t.equal(typeof getGlobalDispatcher, 'function') - t.end() + t = tspl(t, { plan: 10 }) + t.strictEqual(typeof Client, 'function') + t.strictEqual(typeof Pool, 'function') + t.strictEqual(typeof Agent, 'function') + t.strictEqual(typeof request, 'function') + t.strictEqual(typeof stream, 'function') + t.strictEqual(typeof pipeline, 'function') + t.strictEqual(typeof connect, 'function') + t.strictEqual(typeof upgrade, 'function') + t.strictEqual(typeof setGlobalDispatcher, 'function') + t.strictEqual(typeof getGlobalDispatcher, 'function') }) From 8218436c1cb2f3151021f06796a5cb040629f45e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 16:01:02 +0100 Subject: [PATCH 023/123] chore: migrate a batch of tests to node test runner (#2744) --- test/client-pipeline.js | 380 ++++++++++++++++++++-------------- test/client-pipelining.js | 226 +++++++++++--------- test/client-timeout.js | 57 ++--- test/client-upgrade.js | 163 ++++++++------- test/get-head-body.js | 65 +++--- test/http2-alpn.js | 19 +- test/http2.js | 423 ++++++++++++++++++++------------------ test/readable.test.js | 2 +- test/request.js | 181 ++++++++-------- 9 files changed, 838 insertions(+), 678 deletions(-) diff --git a/test/client-pipeline.js b/test/client-pipeline.js index cbc78b9270d..bc2cd1d3a95 100644 --- a/test/client-pipeline.js +++ b/test/client-pipeline.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const EE = require('node:events') const { createServer } = require('node:http') @@ -12,30 +13,30 @@ const { PassThrough } = require('node:stream') -test('pipeline get', (t) => { - t.plan(17) +test('pipeline get', async (t) => { + t = tspl(t, { plan: 17 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(undefined, req.headers['content-length']) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(undefined, req.headers['content-length']) res.setHeader('Content-Type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) { const bufs = [] const signal = new EE() client.pipeline({ signal, path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') + t.strictEqual(signal.listenerCount('abort'), 1) return body }) .end() @@ -43,19 +44,19 @@ test('pipeline get', (t) => { bufs.push(buf) }) .on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) .on('close', () => { - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) } { const bufs = [] client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') return body }) .end() @@ -63,23 +64,25 @@ test('pipeline get', (t) => { bufs.push(buf) }) .on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } }) + + await t.completed }) -test('pipeline echo', (t) => { - t.plan(2) +test('pipeline echo', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let res = '' const buf1 = Buffer.alloc(1e3).toString() @@ -104,19 +107,21 @@ test('pipeline echo', (t) => { callback() }, final (callback) { - t.equal(res, buf1 + buf2) + t.strictEqual(res, buf1 + buf2) callback() } }), (err) => { - t.error(err) + t.ifError(err) } ) }) + + await t.completed }) -test('pipeline ignore request body', (t) => { - t.plan(2) +test('pipeline ignore request body', async (t) => { + t = tspl(t, { plan: 2 }) let done const server = createServer((req, res) => { @@ -124,11 +129,11 @@ test('pipeline ignore request body', (t) => { res.end() done() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let res = '' const buf1 = Buffer.alloc(1e3).toString() @@ -153,71 +158,77 @@ test('pipeline ignore request body', (t) => { callback() }, final (callback) { - t.equal(res, 'asd') + t.strictEqual(res, 'asd') callback() } }), (err) => { - t.error(err) + t.ifError(err) } ) }) + + await t.completed }) -test('pipeline invalid handler', (t) => { - t.plan(1) +test('pipeline invalid handler', async (t) => { + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:5000') client.pipeline({}, null).on('error', (err) => { t.ok(/handler/.test(err)) }) + + await t.completed }) -test('pipeline invalid handler return after destroy should not error', (t) => { - t.plan(3) +test('pipeline invalid handler return after destroy should not error', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const dup = client.pipeline({ path: '/', method: 'GET' }, ({ body }) => { body.on('error', (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) dup.destroy(new Error('asd')) return {} }) .on('error', (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) .on('close', () => { t.ok(true, 'pass') }) .end() }) + + await t.completed }) -test('pipeline error body', (t) => { - t.plan(2) +test('pipeline error body', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const buf = Buffer.alloc(1e6).toString() pipeline( @@ -245,19 +256,21 @@ test('pipeline error body', (t) => { } ) }) + + await t.completed }) -test('pipeline destroy body', (t) => { - t.plan(2) +test('pipeline destroy body', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const buf = Buffer.alloc(1e6).toString() pipeline( @@ -285,19 +298,21 @@ test('pipeline destroy body', (t) => { } ) }) + + await t.completed }) -test('pipeline backpressure', (t) => { - t.plan(1) +test('pipeline backpressure', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const buf = Buffer.alloc(1e6).toString() const duplex = client.pipeline({ @@ -318,19 +333,21 @@ test('pipeline backpressure', (t) => { t.ok(true, 'pass') }) }) + + await t.completed }) -test('pipeline invalid handler return', (t) => { - t.plan(2) +test('pipeline invalid handler return', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -357,19 +374,21 @@ test('pipeline invalid handler return', (t) => { }) .end() }) + + await t.completed }) -test('pipeline throw handler', (t) => { - t.plan(1) +test('pipeline throw handler', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -380,23 +399,25 @@ test('pipeline throw handler', (t) => { throw new Error('asd') }) .on('error', (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) .end() }) + + await t.completed }) -test('pipeline destroy and throw handler', (t) => { - t.plan(2) +test('pipeline destroy and throw handler', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const dup = client.pipeline({ path: '/', @@ -415,21 +436,23 @@ test('pipeline destroy and throw handler', (t) => { t.ok(true, 'pass') }) }) + + await t.completed }) -test('pipeline abort res', (t) => { - t.plan(2) +test('pipeline abort res', async (t) => { + t = tspl(t, { plan: 2 }) let _res const server = createServer((req, res) => { res.write('asd') _res = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -453,19 +476,21 @@ test('pipeline abort res', (t) => { }) .end() }) + + await t.completed }) -test('pipeline abort server res', (t) => { - t.plan(1) +test('pipeline abort server res', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -478,25 +503,27 @@ test('pipeline abort server res', (t) => { }) .end() }) + + await t.completed }) -test('pipeline abort duplex', (t) => { - t.plan(2) +test('pipeline abort duplex', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'PUT' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() client.pipeline({ @@ -509,19 +536,21 @@ test('pipeline abort duplex', (t) => { }) }) }) + + await t.completed }) -test('pipeline abort piped res', (t) => { - t.plan(1) +test('pipeline abort piped res', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -534,23 +563,25 @@ test('pipeline abort piped res', (t) => { return pipeline(body, pt, () => {}) }) .on('error', (err) => { - t.equal(err.code, 'UND_ERR_ABORTED') + t.strictEqual(err.code, 'UND_ERR_ABORTED') }) .end() }) + + await t.completed }) -test('pipeline abort piped res 2', (t) => { - t.plan(2) +test('pipeline abort piped res 2', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -571,19 +602,21 @@ test('pipeline abort piped res 2', (t) => { }) .end() }) + + await t.completed }) -test('pipeline abort piped res 3', (t) => { - t.plan(2) +test('pipeline abort piped res 3', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -591,7 +624,7 @@ test('pipeline abort piped res 3', (t) => { }, ({ body }) => { const pt = new PassThrough() body.on('error', (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) setImmediate(() => { pt.destroy(new Error('asd')) @@ -600,25 +633,27 @@ test('pipeline abort piped res 3', (t) => { return pt }) .on('error', (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) .end() }) + + await t.completed }) -test('pipeline abort server res after headers', (t) => { - t.plan(1) +test('pipeline abort server res after headers', async (t) => { + t = tspl(t, { plan: 1 }) let _res const server = createServer((req, res) => { res.write('asd') _res = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -632,21 +667,23 @@ test('pipeline abort server res after headers', (t) => { }) .end() }) + + await t.completed }) -test('pipeline w/ write abort server res after headers', (t) => { - t.plan(1) +test('pipeline w/ write abort server res after headers', async (t) => { + t = tspl(t, { plan: 1 }) let _res const server = createServer((req, res) => { req.pipe(res) _res = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -661,21 +698,23 @@ test('pipeline w/ write abort server res after headers', (t) => { .resume() .write('asd') }) + + await t.completed }) -test('destroy in push', (t) => { - t.plan(3) +test('destroy in push', async (t) => { + t = tspl(t, { plan: 3 }) let _res const server = createServer((req, res) => { res.write('asd') _res = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.pipeline({ path: '/', method: 'GET' }, ({ body }) => { body.once('data', () => { @@ -698,15 +737,17 @@ test('destroy in push', (t) => { buf = chunk.toString() _res.end() }).on('end', () => { - t.equal('asd', buf) + t.strictEqual('asd', buf) }) return body }).resume().end() }) + + await t.completed }) -test('pipeline args validation', (t) => { - t.plan(2) +test('pipeline args validation', async (t) => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:5000') @@ -715,19 +756,21 @@ test('pipeline args validation', (t) => { t.ok(/opts/.test(err.message)) t.ok(err instanceof errors.InvalidArgumentError) }) + + await t.completed }) -test('pipeline factory throw not unhandled', (t) => { - t.plan(1) +test('pipeline factory throw not unhandled', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -740,19 +783,21 @@ test('pipeline factory throw not unhandled', (t) => { }) .end() }) + + await t.completed }) -test('pipeline destroy before dispatch', (t) => { - t.plan(1) +test('pipeline destroy before dispatch', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client .pipeline({ path: '/', method: 'GET' }, ({ body }) => { @@ -764,10 +809,12 @@ test('pipeline destroy before dispatch', (t) => { .end() .destroy() }) + + await t.completed }) -test('pipeline legacy stream', (t) => { - t.plan(1) +test('pipeline legacy stream', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.write(Buffer.alloc(16e3)) @@ -775,11 +822,11 @@ test('pipeline legacy stream', (t) => { res.end(Buffer.alloc(16e3)) }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client .pipeline({ path: '/', method: 'GET' }, ({ body }) => { @@ -793,19 +840,21 @@ test('pipeline legacy stream', (t) => { }) .end() }) + + await t.completed }) -test('pipeline objectMode', (t) => { - t.plan(1) +test('pipeline objectMode', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end(JSON.stringify({ asd: 1 })) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => { @@ -817,26 +866,28 @@ test('pipeline objectMode', (t) => { }), () => {}) }) .on('data', data => { - t.strictSame(data, { asd: 1 }) + t.deepStrictEqual(data, { asd: 1 }) }) .end() }) + + await t.completed }) -test('pipeline invalid opts', (t) => { - t.plan(2) +test('pipeline invalid opts', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end(JSON.stringify({ asd: 1 })) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.close((err) => { - t.error(err) + t.ifError(err) }) client .pipeline({ path: '/', method: 'GET', objectMode: true }, ({ body }) => { @@ -846,19 +897,21 @@ test('pipeline invalid opts', (t) => { t.ok(err) }) }) + + await t.completed }) -test('pipeline CONNECT throw', (t) => { - t.plan(1) +test('pipeline CONNECT throw', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -872,19 +925,21 @@ test('pipeline CONNECT throw', (t) => { t.fail() }) }) + + await t.completed }) -test('pipeline body without destroy', (t) => { - t.plan(1) +test('pipeline body without destroy', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.pipeline({ path: '/', @@ -900,20 +955,22 @@ test('pipeline body without destroy', (t) => { }) .resume() }) + + await t.completed }) -test('pipeline ignore 1xx', (t) => { - t.plan(1) +test('pipeline ignore 1xx', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.writeProcessing() res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.pipeline({ @@ -924,24 +981,27 @@ test('pipeline ignore 1xx', (t) => { buf += chunk }) .on('end', () => { - t.equal(buf, 'hello') + t.strictEqual(buf, 'hello') }) .end() }) + + await t.completed }) -test('pipeline ignore 1xx and use onInfo', (t) => { - t.plan(3) + +test('pipeline ignore 1xx and use onInfo', async (t) => { + t = tspl(t, { plan: 3 }) const infos = [] const server = createServer((req, res) => { res.writeProcessing() res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.pipeline({ @@ -955,16 +1015,18 @@ test('pipeline ignore 1xx and use onInfo', (t) => { buf += chunk }) .on('end', () => { - t.equal(buf, 'hello') - t.equal(infos.length, 1) - t.equal(infos[0].statusCode, 102) + t.strictEqual(buf, 'hello') + t.strictEqual(infos.length, 1) + t.strictEqual(infos[0].statusCode, 102) }) .end() }) + + await t.completed }) -test('pipeline backpressure', (t) => { - t.plan(1) +test('pipeline backpressure', async (t) => { + t = tspl(t, { plan: 1 }) const expected = Buffer.alloc(1e6).toString() @@ -972,11 +1034,11 @@ test('pipeline backpressure', (t) => { res.writeProcessing() res.end(expected) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.pipeline({ @@ -996,13 +1058,15 @@ test('pipeline backpressure', (t) => { buf += chunk }) .on('end', () => { - t.equal(buf, expected) + t.strictEqual(buf, expected) }) }) + + await t.completed }) -test('pipeline abort after headers', (t) => { - t.plan(1) +test('pipeline abort after headers', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.writeProcessing() @@ -1011,11 +1075,11 @@ test('pipeline abort after headers', (t) => { res.write('asd') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const signal = new EE() client.pipeline({ @@ -1033,4 +1097,6 @@ test('pipeline abort after headers', (t) => { t.ok(err instanceof errors.RequestAbortedError) }) }) + + await t.completed }) diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 25441407f43..475b826c62d 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { finished, Readable } = require('node:stream') @@ -9,9 +10,9 @@ const EE = require('node:events') const { kBusy, kRunning, kSize } = require('../lib/core/symbols') const { maybeWrapStream, consts } = require('./utils/async-iterators') -test('20 times GET with pipelining 10', (t) => { +test('20 times GET with pipelining 10', async (t) => { const num = 20 - t.plan(3 * num + 1) + t = tspl(t, { plan: 3 * num + 1 }) let count = 0 let countGreaterThanOne = false @@ -22,7 +23,7 @@ test('20 times GET with pipelining 10', (t) => { res.end(req.url) }, 10) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) // needed to check for a warning on the maxListeners on the socket function onWarning (warning) { @@ -31,7 +32,7 @@ test('20 times GET with pipelining 10', (t) => { } } process.on('warning', onWarning) - t.teardown(() => { + after(() => { process.removeListener('warning', onWarning) }) @@ -39,7 +40,7 @@ test('20 times GET with pipelining 10', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) for (let i = 0; i < num; i++) { makeRequest(i) @@ -55,28 +56,30 @@ test('20 times GET with pipelining 10', (t) => { }) } }) + + await t.completed }) function makeRequestAndExpectUrl (client, i, t, cb) { return client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => { cb() - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('/' + i, Buffer.concat(bufs).toString('utf8')) + t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8')) }) }) } -test('A client should enqueue as much as twice its pipelining factor', (t) => { +test('A client should enqueue as much as twice its pipelining factor', async (t) => { const num = 10 let sent = 0 // x * 6 + 1 t.ok + 5 drain - t.plan(num * 6 + 1 + 5 + 2) + t = tspl(t, { plan: num * 6 + 1 + 5 + 2 }) let count = 0 let countGreaterThanOne = false @@ -88,22 +91,22 @@ test('A client should enqueue as much as twice its pipelining factor', (t) => { res.end(req.url) }, 10) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) for (; sent < 2;) { - t.notOk(client[kSize] > client.pipelining, 'client is not full') + t.ok(client[kSize] <= client.pipelining, 'client is not full') makeRequest() t.ok(client[kSize] <= client.pipelining, 'we can send more requests') } t.ok(client[kBusy], 'client is busy') - t.notOk(client[kSize] > client.pipelining, 'client is full') + t.ok(client[kSize] <= client.pipelining, 'client is full') makeRequest() t.ok(client[kBusy], 'we must stop now') t.ok(client[kBusy], 'client is busy') @@ -117,7 +120,7 @@ test('A client should enqueue as much as twice its pipelining factor', (t) => { t.ok(countGreaterThanOne, 'seen more than one parallel request') const start = sent for (; sent < start + 2 && sent < num;) { - t.notOk(client[kSize] > client.pipelining, 'client is not full') + t.ok(client[kSize] <= client.pipelining, 'client is not full') t.ok(makeRequest()) } } @@ -126,54 +129,58 @@ test('A client should enqueue as much as twice its pipelining factor', (t) => { return client[kSize] <= client.pipelining } }) + + await t.completed }) -test('pipeline 1 is 1 active request', (t) => { - t.plan(9) +test('pipeline 1 is 1 active request', async (t) => { + t = tspl(t, { plan: 9 }) let res2 const server = createServer((req, res) => { res.write('asd') res2 = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.equal(client[kSize], 1) - t.error(err) - t.notOk(client.request({ + t.strictEqual(client[kSize], 1) + t.ifError(err) + t.strictEqual(client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) finished(data.body, (err) => { t.ok(err) client.close((err) => { - t.error(err) + t.ifError(err) }) }) data.body.destroy() res2.end() - })) + }), undefined) data.body.resume() res2.end() }) t.ok(client[kSize] <= client.pipelining) t.ok(client[kBusy]) - t.equal(client[kSize], 1) + t.strictEqual(client[kSize], 1) }) + + await t.completed }) -test('pipelined chunked POST stream', (t) => { - t.plan(4 + 8 + 8) +test('pipelined chunked POST stream', async (t) => { + t = tspl(t, { plan: 4 + 8 + 8 }) let a = 0 let b = 0 @@ -187,20 +194,20 @@ test('pipelined chunked POST stream', (t) => { res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -213,7 +220,7 @@ test('pipelined chunked POST stream', (t) => { }) }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -221,7 +228,7 @@ test('pipelined chunked POST stream', (t) => { method: 'GET' }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -234,13 +241,15 @@ test('pipelined chunked POST stream', (t) => { }) }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('pipelined chunked POST iterator', (t) => { - t.plan(4 + 8 + 8) +test('pipelined chunked POST iterator', async (t) => { + t = tspl(t, { plan: 4 + 8 + 8 }) let a = 0 let b = 0 @@ -254,20 +263,20 @@ test('pipelined chunked POST iterator', (t) => { res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -280,7 +289,7 @@ test('pipelined chunked POST iterator', (t) => { })() }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -288,7 +297,7 @@ test('pipelined chunked POST iterator', (t) => { method: 'GET' }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) client.request({ @@ -301,14 +310,16 @@ test('pipelined chunked POST iterator', (t) => { })() }, (err, { body }) => { body.resume() - t.error(err) + t.ifError(err) }) }) + + await t.completed }) function errordInflightPost (bodyType) { - test(`errored POST body lets inflight complete ${bodyType}`, (t) => { - t.plan(6) + test(`errored POST body lets inflight complete ${bodyType}`, async (t) => { + t = tspl(t, { plan: 6 }) let serverRes const server = createServer() @@ -316,19 +327,19 @@ function errordInflightPost (bodyType) { serverRes = res res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .once('data', () => { @@ -347,10 +358,10 @@ function errordInflightPost (bodyType) { }), bodyType) }, (err, data) => { t.ok(err) - t.equal(data.opaque, 'asd') + t.strictEqual(data.opaque, 'asd') }) client.close((err) => { - t.error(err) + t.ifError(err) }) serverRes.end() }) @@ -359,14 +370,15 @@ function errordInflightPost (bodyType) { }) }) }) + await t.completed }) } errordInflightPost(consts.STREAM) errordInflightPost(consts.ASYNC_ITERATOR) -test('pipelining non-idempotent', (t) => { - t.plan(4) +test('pipelining non-idempotent', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer() server.on('request', (req, res) => { @@ -374,20 +386,20 @@ test('pipelining non-idempotent', (t) => { res.end('asd') }, 10) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) let ended = false client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -401,16 +413,18 @@ test('pipelining non-idempotent', (t) => { method: 'GET', idempotent: false }, (err, data) => { - t.error(err) - t.equal(ended, true) + t.ifError(err) + t.strictEqual(ended, true) data.body.resume() }) }) + + await t.completed }) function pipeliningNonIdempotentWithBody (bodyType) { - test(`pipelining non-idempotent w body ${bodyType}`, (t) => { - t.plan(4) + test(`pipelining non-idempotent w body ${bodyType}`, async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer() server.on('request', (req, res) => { @@ -418,13 +432,13 @@ function pipeliningNonIdempotentWithBody (bodyType) { res.end('asd') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) let ended = false let reading = false @@ -445,7 +459,7 @@ function pipeliningNonIdempotentWithBody (bodyType) { } }), bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -458,11 +472,13 @@ function pipeliningNonIdempotentWithBody (bodyType) { method: 'GET', idempotent: false }, (err, data) => { - t.error(err) - t.equal(ended, true) + t.ifError(err) + t.strictEqual(ended, true) data.body.resume() }) }) + + await t.completed }) } @@ -470,25 +486,25 @@ pipeliningNonIdempotentWithBody(consts.STREAM) pipeliningNonIdempotentWithBody(consts.ASYNC_ITERATOR) function pipeliningHeadBusy (bodyType) { - test(`pipelining HEAD busy ${bodyType}`, (t) => { - t.plan(7) + test(`pipelining HEAD busy ${bodyType}`, async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer() server.on('request', (req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client[kConnect](() => { let ended = false client.once('disconnect', () => { - t.equal(ended, true) + t.strictEqual(ended, true) }) { @@ -500,7 +516,7 @@ function pipeliningHeadBusy (bodyType) { method: 'GET', body: maybeWrapStream(body, bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -508,7 +524,7 @@ function pipeliningHeadBusy (bodyType) { }) }) body.push(null) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } { @@ -520,7 +536,7 @@ function pipeliningHeadBusy (bodyType) { method: 'HEAD', body: maybeWrapStream(body, bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -529,18 +545,20 @@ function pipeliningHeadBusy (bodyType) { }) }) body.push(null) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } }) }) + + await t.completed }) } pipeliningHeadBusy(consts.STREAM) pipeliningHeadBusy(consts.ASYNC_ITERATOR) -test('pipelining empty pipeline before reset', (t) => { - t.plan(8) +test('pipelining empty pipeline before reset', async (t) => { + t = tspl(t, { plan: 8 }) let c = 0 const server = createServer() @@ -553,39 +571,39 @@ test('pipelining empty pipeline before reset', (t) => { }, 100) } }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client[kConnect](() => { let ended = false client.once('disconnect', () => { - t.equal(ended, true) + t.strictEqual(ended, true) }) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { t.ok(true, 'pass') }) }) - t.equal(client[kBusy], false) + t.strictEqual(client[kBusy], false) client.request({ path: '/', method: 'HEAD', body: 'asd' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -593,27 +611,29 @@ test('pipelining empty pipeline before reset', (t) => { t.ok(true, 'pass') }) }) - t.equal(client[kBusy], true) - t.equal(client[kRunning], 2) + t.strictEqual(client[kBusy], true) + t.strictEqual(client[kRunning], 2) }) }) + + await t.completed }) function pipeliningIdempotentBusy (bodyType) { - test(`pipelining idempotent busy ${bodyType}`, (t) => { - t.plan(12) + test(`pipelining idempotent busy ${bodyType}`, async (t) => { + t = tspl(t, { plan: 12 }) const server = createServer() server.on('request', (req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) { const body = new Readable({ @@ -624,7 +644,7 @@ function pipeliningIdempotentBusy (bodyType) { method: 'GET', body: maybeWrapStream(body, bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -632,7 +652,7 @@ function pipeliningIdempotentBusy (bodyType) { }) }) body.push(null) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } client[kConnect](() => { @@ -645,7 +665,7 @@ function pipeliningIdempotentBusy (bodyType) { method: 'GET', body: maybeWrapStream(body, bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -653,7 +673,7 @@ function pipeliningIdempotentBusy (bodyType) { }) }) body.push(null) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } { @@ -669,9 +689,9 @@ function pipeliningIdempotentBusy (bodyType) { }, (err, data) => { t.ok(err) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) signal.emit('abort') - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } { @@ -684,7 +704,7 @@ function pipeliningIdempotentBusy (bodyType) { idempotent: false, body: maybeWrapStream(body, bodyType) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -692,18 +712,20 @@ function pipeliningIdempotentBusy (bodyType) { }) }) body.push(null) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) } }) }) + + await t.completed }) } pipeliningIdempotentBusy(consts.STREAM) pipeliningIdempotentBusy(consts.ASYNC_ITERATOR) -test('pipelining blocked', (t) => { - t.plan(6) +test('pipelining blocked', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer() @@ -717,19 +739,19 @@ test('pipelining blocked', (t) => { res.end('asd') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', blocking: true }, (err, data) => { - t.error(err) + t.ifError(err) blocking = false data.body .resume() @@ -741,7 +763,7 @@ test('pipelining blocked', (t) => { path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { @@ -749,4 +771,6 @@ test('pipelining blocked', (t) => { }) }) }) + + await t.completed }) diff --git a/test/client-timeout.js b/test/client-timeout.js index dd5b1da6505..4964774d0bc 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -1,25 +1,26 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const FakeTimers = require('@sinonjs/fake-timers') const timers = require('../lib/timers') -test('refresh timeout on pause', (t) => { - t.plan(1) +test('refresh timeout on pause', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.flushHeaders() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 500 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.dispatch({ path: '/', @@ -44,30 +45,32 @@ test('refresh timeout on pause', (t) => { } }) }) + + await t.completed }) -test('start headers timeout after request body', (t) => { - t.plan(2) +test('start headers timeout after request body', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0, headersTimeout: 100 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const body = new Readable({ read () {} }) client.dispatch({ @@ -100,30 +103,32 @@ test('start headers timeout after request body', (t) => { } }) }) + + await t.completed }) -test('start headers timeout after async iterator request body', (t) => { - t.plan(1) +test('start headers timeout after async iterator request body', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0, headersTimeout: 100 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) let res const body = (async function * () { await new Promise((resolve) => { res = resolve }) @@ -157,21 +162,23 @@ test('start headers timeout after async iterator request body', (t) => { } }) }) + + await t.completed }) -test('parser resume with no body timeout', (t) => { - t.plan(1) +test('parser resume with no body timeout', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.dispatch({ path: '/', @@ -190,8 +197,10 @@ test('parser resume with no body timeout', (t) => { t.ok(true, 'pass') }, onError (err) { - t.error(err) + t.ifError(err) } }) }) + + await t.completed }) diff --git a/test/client-upgrade.js b/test/client-upgrade.js index c33ab183363..5cf5e553ba7 100644 --- a/test/client-upgrade.js +++ b/test/client-upgrade.js @@ -1,14 +1,15 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const net = require('node:net') const http = require('node:http') const EE = require('node:events') const { kBusy } = require('../lib/core/symbols') -test('basic upgrade', (t) => { - t.plan(6) +test('basic upgrade', async (t) => { + t = tspl(t, { plan: 6 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -25,11 +26,11 @@ test('basic upgrade', (t) => { c.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.upgrade({ @@ -38,9 +39,9 @@ test('basic upgrade', (t) => { method: 'GET', protocol: 'Websocket' }, (err, data) => { - t.error(err) + t.ifError(err) - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) const { headers, socket } = data @@ -50,22 +51,24 @@ test('basic upgrade', (t) => { }) socket.on('close', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) - t.same(headers, { + t.deepStrictEqual(headers, { hello: 'world', connection: 'upgrade', upgrade: 'websocket' }) socket.end() }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('basic upgrade promise', (t) => { - t.plan(2) +test('basic upgrade promise', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -81,11 +84,11 @@ test('basic upgrade promise', (t) => { c.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { headers, socket } = await client.upgrade({ path: '/', @@ -99,20 +102,22 @@ test('basic upgrade promise', (t) => { }) socket.on('close', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) - t.same(headers, { + t.deepStrictEqual(headers, { hello: 'world', connection: 'upgrade', upgrade: 'websocket' }) socket.end() }) + + await t.completed }) -test('upgrade error', (t) => { - t.plan(1) +test('upgrade error', async (t) => { + t = tspl(t, { plan: 1 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -127,11 +132,11 @@ test('upgrade error', (t) => { // Ignore error. }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { await client.upgrade({ @@ -143,16 +148,18 @@ test('upgrade error', (t) => { t.ok(err) } }) + + await t.completed }) -test('upgrade invalid opts', (t) => { - t.plan(6) +test('upgrade invalid opts', async (t) => { + t = tspl(t, { plan: 6 }) const client = new Client('http://localhost:5432') client.upgrade(null, err => { t.ok(err instanceof errors.InvalidArgumentError) - t.equal(err.message, 'invalid opts') + t.strictEqual(err.message, 'invalid opts') }) try { @@ -160,7 +167,7 @@ test('upgrade invalid opts', (t) => { t.fail() } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) - t.equal(err.message, 'invalid opts') + t.strictEqual(err.message, 'invalid opts') } try { @@ -168,12 +175,12 @@ test('upgrade invalid opts', (t) => { t.fail() } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) - t.equal(err.message, 'invalid callback') + t.strictEqual(err.message, 'invalid callback') } }) -test('basic upgrade2', (t) => { - t.plan(3) +test('basic upgrade2', async (t) => { + t = tspl(t, { plan: 3 }) const server = http.createServer() server.on('upgrade', (req, c, head) => { @@ -185,18 +192,18 @@ test('basic upgrade2', (t) => { c.write('Body') c.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.upgrade({ path: '/', method: 'GET', protocol: 'Websocket' }, (err, data) => { - t.error(err) + t.ifError(err) const { headers, socket } = data @@ -206,10 +213,10 @@ test('basic upgrade2', (t) => { }) socket.on('close', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) - t.same(headers, { + t.deepStrictEqual(headers, { hello: 'world', connection: 'upgrade', upgrade: 'websocket' @@ -217,10 +224,12 @@ test('basic upgrade2', (t) => { socket.end() }) }) + + await t.completed }) -test('upgrade wait for empty pipeline', (t) => { - t.plan(7) +test('upgrade wait for empty pipeline', async (t) => { + t = tspl(t, { plan: 7 }) let canConnect = false const server = http.createServer((req, res) => { @@ -228,7 +237,7 @@ test('upgrade wait for empty pipeline', (t) => { canConnect = true }) server.on('upgrade', (req, c, firstBodyChunk) => { - t.equal(canConnect, true) + t.strictEqual(canConnect, true) c.write('HTTP/1.1 101\r\n') c.write('hello: world\r\n') c.write('connection: upgrade\r\n') @@ -237,55 +246,57 @@ test('upgrade wait for empty pipeline', (t) => { c.write('Body') c.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) client.once('connect', () => { process.nextTick(() => { - t.equal(client[kBusy], false) + t.strictEqual(client[kBusy], false) client.upgrade({ path: '/' }, (err, { socket }) => { - t.error(err) + t.ifError(err) let recvData = '' socket.on('data', (d) => { recvData += d }) socket.on('end', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) socket.write('Body') socket.end() }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) }) }) }) + + await t.completed }) -test('upgrade aborted', (t) => { - t.plan(6) +test('upgrade aborted', async (t) => { + t = tspl(t, { plan: 6 }) const server = http.createServer((req, res) => { t.fail() @@ -293,13 +304,13 @@ test('upgrade aborted', (t) => { server.on('upgrade', (req, c, firstBodyChunk) => { t.fail() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const signal = new EE() client.upgrade({ @@ -307,22 +318,24 @@ test('upgrade aborted', (t) => { signal, opaque: 'asd' }, (err, { opaque }) => { - t.equal(opaque, 'asd') + t.strictEqual(opaque, 'asd') t.ok(err instanceof errors.RequestAbortedError) - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) }) - t.equal(client[kBusy], true) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(client[kBusy], true) + t.strictEqual(signal.listenerCount('abort'), 1) signal.emit('abort') client.close(() => { t.ok(true, 'pass') }) }) + + await t.completed }) -test('basic aborted after res', (t) => { - t.plan(1) +test('basic aborted after res', async (t) => { + t = tspl(t, { plan: 1 }) const signal = new EE() const server = http.createServer() @@ -339,11 +352,11 @@ test('basic aborted after res', (t) => { }) signal.emit('abort') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.upgrade({ path: '/', @@ -354,10 +367,12 @@ test('basic aborted after res', (t) => { t.ok(err instanceof errors.RequestAbortedError) }) }) + + await t.completed }) -test('basic upgrade error', (t) => { - t.plan(2) +test('basic upgrade error', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -372,11 +387,11 @@ test('basic upgrade error', (t) => { }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const _err = new Error() client.upgrade({ @@ -384,30 +399,32 @@ test('basic upgrade error', (t) => { method: 'GET', protocol: 'Websocket' }, (err, data) => { - t.error(err) + t.ifError(err) data.socket.on('error', (err) => { - t.equal(err, _err) + t.strictEqual(err, _err) }) throw _err }) }) + + await t.completed }) -test('upgrade disconnect', (t) => { - t.plan(3) +test('upgrade disconnect', async (t) => { + t = tspl(t, { plan: 3 }) const server = net.createServer(connection => { connection.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.on('disconnect', (origin, [self], error) => { - t.equal(client, self) + t.strictEqual(client, self) t.ok(error instanceof Error) }) @@ -420,19 +437,21 @@ test('upgrade disconnect', (t) => { t.ok(error instanceof Error) }) }) + + await t.completed }) -test('upgrade invalid signal', (t) => { - t.plan(2) +test('upgrade invalid signal', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(() => { t.fail() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.on('disconnect', () => { t.fail() @@ -445,8 +464,10 @@ test('upgrade invalid signal', (t) => { signal: 'error', opaque: 'asd' }, (err, { opaque }) => { - t.equal(opaque, 'asd') + t.strictEqual(opaque, 'asd') t.ok(err instanceof errors.InvalidArgumentError) }) }) + + await t.completed }) diff --git a/test/get-head-body.js b/test/get-head-body.js index 74420a174cd..4e58bc0f5a5 100644 --- a/test/get-head-body.js +++ b/test/get-head-body.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') @@ -8,17 +9,17 @@ const { kConnect } = require('../lib/core/symbols') const { kBusy } = require('../lib/core/symbols') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -test('GET and HEAD with body should reset connection', (t) => { - t.plan(8 + 2) +test('GET and HEAD with body should reset connection', async (t) => { + t = tspl(t, { plan: 8 + 2 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.on('disconnect', () => { t.ok(true, 'pass') @@ -29,7 +30,7 @@ test('GET and HEAD with body should reset connection', (t) => { body: 'asd', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -42,7 +43,7 @@ test('GET and HEAD with body should reset connection', (t) => { body: emptyBody, method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -55,7 +56,7 @@ test('GET and HEAD with body should reset connection', (t) => { }), method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -69,7 +70,7 @@ test('GET and HEAD with body should reset connection', (t) => { }), method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -78,7 +79,7 @@ test('GET and HEAD with body should reset connection', (t) => { body: [], method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -91,7 +92,7 @@ test('GET and HEAD with body should reset connection', (t) => { })), method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -105,44 +106,48 @@ test('GET and HEAD with body should reset connection', (t) => { })), method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) }) + + await t.completed }) // TODO: Avoid external dependency. -// test('GET with body should work when target parses body as request', (t) => { -// t.plan(4) +// test('GET with body should work when target parses body as request', async (t) => { +// t = tspl(t, { plan: 4 }) // // This URL will send double responses when receiving a // // GET request with body. // const client = new Client('http://feeds.bbci.co.uk') -// t.teardown(client.close.bind(client)) +// after(() => client.close()) // client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => { -// t.error(err) -// t.equal(data.statusCode, 200) +// t.ifError(err) +// t.strictEqual(data.statusCode, 200) // data.body.resume() // }) // client.request({ method: 'GET', path: '/news/rss.xml', body: 'asd' }, (err, data) => { -// t.error(err) -// t.equal(data.statusCode, 200) +// t.ifError(err) +// t.strictEqual(data.statusCode, 200) // data.body.resume() // }) + +// await t.completed // }) -test('HEAD should reset connection', (t) => { - t.plan(8) +test('HEAD should reset connection', async (t) => { + t = tspl(t, { plan: 8 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.once('disconnect', () => { t.ok(true, 'pass') @@ -152,16 +157,16 @@ test('HEAD should reset connection', (t) => { path: '/', method: 'HEAD' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) client.request({ path: '/', method: 'HEAD' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() client.once('disconnect', () => { client[kConnect](() => { @@ -169,16 +174,18 @@ test('HEAD should reset connection', (t) => { path: '/', method: 'HEAD' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() data.body.on('end', () => { t.ok(true, 'pass') }) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) }) }) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) }) + + await t.completed }) diff --git a/test/http2-alpn.js b/test/http2-alpn.js index 04b8cb6abd8..590f65e128e 100644 --- a/test/http2-alpn.js +++ b/test/http2-alpn.js @@ -1,11 +1,12 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const https = require('node:https') const { once } = require('node:events') const { createSecureServer } = require('node:http2') const { readFileSync } = require('node:fs') const { join } = require('node:path') -const { test } = require('tap') const { Client } = require('..') @@ -15,7 +16,7 @@ const cert = readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8') const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => { - t.plan(10) + t = tspl(t, { plan: 10 }) const body = [] const httpsBody = [] @@ -47,7 +48,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => await once(server, 'listening') // close the server on teardown - t.teardown(server.close.bind(server)) + after(() => server.close()) // set the port const port = server.address().port @@ -62,7 +63,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => }) // close the client on teardown - t.teardown(client.close.bind(client)) + after(() => client.close()) // make an undici request using where it wants http/2 const response = await client.request({ @@ -110,7 +111,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => reject(err) }) - t.teardown(httpsRequest.destroy.bind(httpsRequest)) + after(() => httpsRequest.destroy()) }) t.equal(httpsResponse.statusCode, 200) @@ -124,7 +125,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => }) test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => { - t.plan(15) + t = tspl(t, { plan: 15 }) const requestChunks = [] const responseBody = [] @@ -194,7 +195,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => await once(server, 'listening') // close the server on teardown - t.teardown(server.close.bind(server)) + after(() => server.close()) // set the port const port = server.address().port @@ -209,7 +210,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => }) // close the client on teardown - t.teardown(client.close.bind(client)) + after(() => client.close()) // make an undici request using where it wants http/2 const response = await client.request({ @@ -265,7 +266,7 @@ test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => httpsRequest.write(Buffer.from(body)) - t.teardown(httpsRequest.destroy.bind(httpsRequest)) + after(() => httpsRequest.destroy()) }) t.equal(httpsResponse.statusCode, 201) diff --git a/test/http2.js b/test/http2.js index 538c6677788..3319ffddb1e 100644 --- a/test/http2.js +++ b/test/http2.js @@ -1,27 +1,26 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createSecureServer } = require('node:http2') const { createReadStream, readFileSync } = require('node:fs') const { once } = require('node:events') const { Blob } = require('node:buffer') const { Writable, pipeline, PassThrough, Readable } = require('node:stream') -const { test, plan } = require('tap') const pem = require('https-pem') const { Client, Agent } = require('..') const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20 -plan(25) - test('Should support H2 connection', async t => { const body = [] const server = createSecureServer(pem) server.on('stream', (stream, headers, _flags, rawHeaders) => { - t.equal(headers['x-my-header'], 'foo') - t.equal(headers[':method'], 'GET') + t.strictEqual(headers['x-my-header'], 'foo') + t.strictEqual(headers[':method'], 'GET') stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -40,9 +39,9 @@ test('Should support H2 connection', async t => { allowH2: true }) - t.plan(6) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 6 }) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -57,23 +56,23 @@ test('Should support H2 connection', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!') }) test('Should support H2 connection(multiple requests)', async t => { const server = createSecureServer(pem) server.on('stream', async (stream, headers, _flags, rawHeaders) => { - t.equal(headers['x-my-header'], 'foo') - t.equal(headers[':method'], 'POST') + t.strictEqual(headers['x-my-header'], 'foo') + t.strictEqual(headers[':method'], 'POST') const reqData = [] stream.on('data', chunk => reqData.push(chunk.toString())) await once(stream, 'end') const reqBody = reqData.join('') - t.equal(reqBody.length > 0, true) + t.strictEqual(reqBody.length > 0, true) stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -92,9 +91,9 @@ test('Should support H2 connection(multiple requests)', async t => { allowH2: true }) - t.plan(21) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 21 }) + after(() => server.close()) + after(() => client.close()) for (let i = 0; i < 3; i++) { const sendBody = `seq ${i}` @@ -114,10 +113,10 @@ test('Should support H2 connection(multiple requests)', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`) } }) @@ -126,9 +125,9 @@ test('Should support H2 connection (headers as array)', async t => { const server = createSecureServer(pem) server.on('stream', (stream, headers) => { - t.equal(headers['x-my-header'], 'foo') - t.equal(headers['x-my-drink'], 'coffee,tea') - t.equal(headers[':method'], 'GET') + t.strictEqual(headers['x-my-header'], 'foo') + t.strictEqual(headers['x-my-drink'], 'coffee,tea') + t.strictEqual(headers[':method'], 'GET') stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -147,9 +146,9 @@ test('Should support H2 connection (headers as array)', async t => { allowH2: true }) - t.plan(7) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 7 }) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -162,21 +161,21 @@ test('Should support H2 connection (headers as array)', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!') }) test('Should support H2 connection(POST Buffer)', async t => { const server = createSecureServer({ ...pem, allowHTTP1: false }) server.on('stream', async (stream, headers, _flags, rawHeaders) => { - t.equal(headers[':method'], 'POST') + t.strictEqual(headers[':method'], 'POST') const reqData = [] stream.on('data', chunk => reqData.push(chunk.toString())) await once(stream, 'end') - t.equal(reqData.join(''), 'hello!') + t.strictEqual(reqData.join(''), 'hello!') stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -195,9 +194,9 @@ test('Should support H2 connection(POST Buffer)', async t => { allowH2: true }) - t.plan(6) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 6 }) + after(() => server.close()) + after(() => client.close()) const sendBody = 'hello!' const body = [] @@ -212,10 +211,10 @@ test('Should support H2 connection(POST Buffer)', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!') }) test('Should support H2 GOAWAY (server-side)', async t => { @@ -223,8 +222,8 @@ test('Should support H2 GOAWAY (server-side)', async t => { const server = createSecureServer(pem) server.on('stream', (stream, headers) => { - t.equal(headers['x-my-header'], 'foo') - t.equal(headers[':method'], 'GET') + t.strictEqual(headers['x-my-header'], 'foo') + t.strictEqual(headers[':method'], 'GET') stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -249,9 +248,9 @@ test('Should support H2 GOAWAY (server-side)', async t => { allowH2: true }) - t.plan(9) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 9 }) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -266,19 +265,21 @@ test('Should support H2 GOAWAY (server-side)', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!') const [url, disconnectClient, err] = await once(client, 'disconnect') t.ok(url instanceof URL) - t.same(disconnectClient, [client]) - t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204') + t.deepStrictEqual(disconnectClient, [client]) + t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 204') }) test('Should throw if bad allowH2 has been passed', async t => { + t = tspl(t, { plan: 1 }) + try { // eslint-disable-next-line new Client('https://localhost:1000', { @@ -286,11 +287,13 @@ test('Should throw if bad allowH2 has been passed', async t => { }) t.fail() } catch (error) { - t.equal(error.message, 'allowH2 must be a valid boolean value') + t.strictEqual(error.message, 'allowH2 must be a valid boolean value') } }) test('Should throw if bad maxConcurrentStreams has been passed', async t => { + t = tspl(t, { plan: 2 }) + try { // eslint-disable-next-line new Client('https://localhost:1000', { @@ -299,7 +302,7 @@ test('Should throw if bad maxConcurrentStreams has been passed', async t => { }) t.fail() } catch (error) { - t.equal( + t.strictEqual( error.message, 'maxConcurrentStreams must be a positive integer, greater than 0' ) @@ -313,17 +316,21 @@ test('Should throw if bad maxConcurrentStreams has been passed', async t => { }) t.fail() } catch (error) { - t.equal( + t.strictEqual( error.message, 'maxConcurrentStreams must be a positive integer, greater than 0' ) } + + await t.completed }) test( 'Request should fail if allowH2 is false and server advertises h1 only', { skip: isGreaterThanv20 }, async t => { + t = tspl(t, { plan: 1 }) + const server = createSecureServer( { ...pem, @@ -345,8 +352,8 @@ test( } }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -356,7 +363,7 @@ test( } }) - t.equal(response.statusCode, 403) + t.strictEqual(response.statusCode, 403) } ) @@ -385,9 +392,9 @@ test( } }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) - t.plan(2) + after(() => server.close()) + after(() => client.close()) + t = tspl(t, { plan: 2 }) try { await client.request({ @@ -398,11 +405,11 @@ test( } }) } catch (error) { - t.equal( + t.strictEqual( error.message, 'Client network socket disconnected before secure TLS connection was established' ) - t.equal(error.code, 'ECONNRESET') + t.strictEqual(error.code, 'ECONNRESET') } } ) @@ -413,9 +420,9 @@ test('Should handle h2 continue', async t => { const responseBody = [] server.on('checkContinue', (request, response) => { - t.equal(request.headers.expect, '100-continue') - t.equal(request.headers['x-my-header'], 'foo') - t.equal(request.headers[':method'], 'POST') + t.strictEqual(request.headers.expect, '100-continue') + t.strictEqual(request.headers['x-my-header'], 'foo') + t.strictEqual(request.headers[':method'], 'POST') response.writeContinue() request.on('data', chunk => requestBody.push(chunk)) @@ -427,7 +434,7 @@ test('Should handle h2 continue', async t => { response.end('hello h2!') }) - t.plan(7) + t = tspl(t, { plan: 7 }) server.listen(0) await once(server, 'listening') @@ -440,8 +447,8 @@ test('Should handle h2 continue', async t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -458,13 +465,13 @@ test('Should handle h2 continue', async t => { await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') }) -test('Dispatcher#Stream', t => { +test('Dispatcher#Stream', async t => { const server = createSecureServer(pem) const expectedBody = 'hello from client!' const bufs = [] @@ -480,7 +487,7 @@ test('Dispatcher#Stream', t => { stream.end('hello h2!') }) - t.plan(4) + t = tspl(t, { plan: 4 }) server.listen(0, async () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -490,14 +497,14 @@ test('Dispatcher#Stream', t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) await client.stream( { path: '/', opaque: { bufs }, method: 'POST', body: expectedBody }, ({ statusCode, headers, opaque: { bufs } }) => { - t.equal(statusCode, 200) - t.equal(headers['x-custom'], 'custom-header') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['x-custom'], 'custom-header') return new Writable({ write (chunk, _encoding, cb) { @@ -508,12 +515,14 @@ test('Dispatcher#Stream', t => { } ) - t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') - t.equal(requestBody, expectedBody) + t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') + t.strictEqual(requestBody, expectedBody) }) + + await t.completed }) -test('Dispatcher#Pipeline', t => { +test('Dispatcher#Pipeline', async t => { const server = createSecureServer(pem) const expectedBody = 'hello from client!' const bufs = [] @@ -529,7 +538,7 @@ test('Dispatcher#Pipeline', t => { stream.end('hello h2!') }) - t.plan(5) + t = tspl(t, { plan: 5 }) server.listen(0, () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -539,8 +548,8 @@ test('Dispatcher#Pipeline', t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) pipeline( new Readable({ @@ -552,8 +561,8 @@ test('Dispatcher#Pipeline', t => { client.pipeline( { path: '/', method: 'POST', body: expectedBody }, ({ statusCode, headers, body }) => { - t.equal(statusCode, 200) - t.equal(headers['x-custom'], 'custom-header') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['x-custom'], 'custom-header') return pipeline(body, new PassThrough(), () => {}) } @@ -565,15 +574,17 @@ test('Dispatcher#Pipeline', t => { } }), err => { - t.error(err) - t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') - t.equal(requestBody, expectedBody) + t.ifError(err) + t.strictEqual(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') + t.strictEqual(requestBody, expectedBody) } ) }) + + await t.completed }) -test('Dispatcher#Connect', t => { +test('Dispatcher#Connect', async t => { const server = createSecureServer(pem) const expectedBody = 'hello from client!' let requestBody = '' @@ -588,7 +599,7 @@ test('Dispatcher#Connect', t => { stream.end('hello h2!') }) - t.plan(6) + t = tspl(t, { plan: 6 }) server.listen(0, () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -598,19 +609,19 @@ test('Dispatcher#Connect', t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) let result = '' client.connect({ path: '/' }, (err, { socket }) => { - t.error(err) + t.ifError(err) socket.on('data', chunk => { result += chunk }) socket.on('response', headers => { - t.equal(headers[':status'], 200) - t.equal(headers['x-custom'], 'custom-header') - t.notOk(socket.closed) + t.strictEqual(headers[':status'], 200) + t.strictEqual(headers['x-custom'], 'custom-header') + t.strictEqual(socket.closed, false) }) // We need to handle the error event although @@ -620,22 +631,24 @@ test('Dispatcher#Connect', t => { socket.on('error', () => {}) socket.once('end', () => { - t.equal(requestBody, expectedBody) - t.equal(result, 'hello h2!') + t.strictEqual(requestBody, expectedBody) + t.strictEqual(result, 'hello h2!') }) socket.end(expectedBody) }) }) + + await t.completed }) -test('Dispatcher#Upgrade', t => { +test('Dispatcher#Upgrade', async t => { const server = createSecureServer(pem) server.on('stream', async (stream, headers) => { stream.end() }) - t.plan(1) + t = tspl(t, { plan: 1 }) server.listen(0, async () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -645,15 +658,17 @@ test('Dispatcher#Upgrade', t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) try { await client.upgrade({ path: '/' }) } catch (error) { - t.equal(error.message, 'Upgrade not supported for H2') + t.strictEqual(error.message, 'Upgrade not supported for H2') } }) + + await t.completed }) test('Dispatcher#destroy', async t => { @@ -674,8 +689,8 @@ test('Dispatcher#destroy', async t => { allowH2: true }) - t.plan(4) - t.teardown(server.close.bind(server)) + t = tspl(t, { plan: 4 }) + after(() => server.close()) promises.push( client.request({ @@ -721,10 +736,10 @@ test('Dispatcher#destroy', async t => { const results = await Promise.allSettled(promises) - t.equal(results[0].status, 'rejected') - t.equal(results[1].status, 'rejected') - t.equal(results[2].status, 'rejected') - t.equal(results[3].status, 'rejected') + t.strictEqual(results[0].status, 'rejected') + t.strictEqual(results[1].status, 'rejected') + t.strictEqual(results[2].status, 'rejected') + t.strictEqual(results[3].status, 'rejected') }) test('Should handle h2 request without body', async t => { @@ -734,9 +749,9 @@ test('Should handle h2 request without body', async t => { const responseBody = [] server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'POST') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + t.strictEqual(headers[':method'], 'POST') + t.strictEqual(headers[':path'], '/') + t.strictEqual(headers[':scheme'], 'https') stream.respond({ 'content-type': 'text/plain; charset=utf-8', @@ -751,7 +766,7 @@ test('Should handle h2 request without body', async t => { stream.end('hello h2!') }) - t.plan(9) + t = tspl(t, { plan: 9 }) server.listen(0) await once(server, 'listening') @@ -763,8 +778,8 @@ test('Should handle h2 request without body', async t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -778,15 +793,15 @@ test('Should handle h2 request without body', async t => { responseBody.push(chunk) } - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') - t.equal(requestChunks.length, 0) - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(requestChunks.length, 0) + t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) -test('Should handle h2 request with body (string or buffer) - dispatch', t => { +test('Should handle h2 request with body (string or buffer) - dispatch', async t => { const server = createSecureServer(pem) const expectedBody = 'hello from client!' const response = [] @@ -804,7 +819,7 @@ test('Should handle h2 request with body (string or buffer) - dispatch', t => { stream.end('hello h2!') }) - t.plan(7) + t = tspl(t, { plan: 7 }) server.listen(0, () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -814,8 +829,8 @@ test('Should handle h2 request with body (string or buffer) - dispatch', t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) client.dispatch( { @@ -832,22 +847,22 @@ test('Should handle h2 request with body (string or buffer) - dispatch', t => { t.ok(true, 'pass') }, onError (err) { - t.error(err) + t.ifError(err) }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain; charset=utf-8') - t.equal(headers['x-custom-h2'], 'foo') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(headers['x-custom-h2'], 'foo') }, onData (chunk) { response.push(chunk) }, onBodySent (body) { - t.equal(body.toString('utf-8'), expectedBody) + t.strictEqual(body.toString('utf-8'), expectedBody) }, onComplete () { - t.equal(Buffer.concat(response).toString('utf-8'), 'hello h2!') - t.equal( + t.strictEqual(Buffer.concat(response).toString('utf-8'), 'hello h2!') + t.strictEqual( Buffer.concat(requestBody).toString('utf-8'), 'hello from client!' ) @@ -855,6 +870,8 @@ test('Should handle h2 request with body (string or buffer) - dispatch', t => { } ) }) + + await t.completed }) test('Should handle h2 request with body (stream)', async t => { @@ -865,9 +882,9 @@ test('Should handle h2 request with body (stream)', async t => { const responseBody = [] server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'PUT') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + t.strictEqual(headers[':method'], 'PUT') + t.strictEqual(headers[':path'], '/') + t.strictEqual(headers[':scheme'], 'https') stream.respond({ 'content-type': 'text/plain; charset=utf-8', @@ -882,7 +899,7 @@ test('Should handle h2 request with body (stream)', async t => { stream.end('hello h2!') }) - t.plan(8) + t = tspl(t, { plan: 8 }) server.listen(0) await once(server, 'listening') @@ -894,8 +911,8 @@ test('Should handle h2 request with body (stream)', async t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -910,11 +927,11 @@ test('Should handle h2 request with body (stream)', async t => { responseBody.push(chunk) } - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) test('Should handle h2 request with body (iterable)', async t => { @@ -934,9 +951,9 @@ test('Should handle h2 request with body (iterable)', async t => { } server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'POST') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + t.strictEqual(headers[':method'], 'POST') + t.strictEqual(headers[':path'], '/') + t.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -949,7 +966,7 @@ test('Should handle h2 request with body (iterable)', async t => { stream.end('hello h2!') }) - t.plan(8) + t = tspl(t, { plan: 8 }) server.listen(0) await once(server, 'listening') @@ -961,8 +978,8 @@ test('Should handle h2 request with body (iterable)', async t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -979,11 +996,11 @@ test('Should handle h2 request with body (iterable)', async t => { await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { @@ -996,9 +1013,9 @@ test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { }) server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'POST') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + t.strictEqual(headers[':method'], 'POST') + t.strictEqual(headers[':path'], '/') + t.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -1011,7 +1028,7 @@ test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { stream.end('hello h2!') }) - t.plan(8) + t = tspl(t, { plan: 8 }) server.listen(0) await once(server, 'listening') @@ -1023,8 +1040,8 @@ test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -1041,11 +1058,11 @@ test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) test( @@ -1062,9 +1079,9 @@ test( buf.copy(new Uint8Array(body)) server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'POST') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + t.strictEqual(headers[':method'], 'POST') + t.strictEqual(headers[':path'], '/') + t.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -1077,7 +1094,7 @@ test( stream.end('hello h2!') }) - t.plan(8) + t = tspl(t, { plan: 8 }) server.listen(0) await once(server, 'listening') @@ -1089,8 +1106,8 @@ test( allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -1107,11 +1124,11 @@ test( await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'foo') - t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'foo') + t.strictEqual(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) } ) @@ -1120,8 +1137,8 @@ test('Agent should support H2 connection', async t => { const server = createSecureServer(pem) server.on('stream', (stream, headers) => { - t.equal(headers['x-my-header'], 'foo') - t.equal(headers[':method'], 'GET') + t.strictEqual(headers['x-my-header'], 'foo') + t.strictEqual(headers[':method'], 'GET') stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': 'hello', @@ -1140,9 +1157,9 @@ test('Agent should support H2 connection', async t => { allowH2: true }) - t.plan(6) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 6 }) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ origin: `https://localhost:${server.address().port}`, @@ -1158,18 +1175,20 @@ test('Agent should support H2 connection', async t => { }) await once(response.body, 'end') - t.equal(response.statusCode, 200) - t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') - t.equal(response.headers['x-custom-h2'], 'hello') - t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8') + t.strictEqual(response.headers['x-custom-h2'], 'hello') + t.strictEqual(Buffer.concat(body).toString('utf8'), 'hello h2!') }) test( 'Should provide pseudo-headers in proper order', async t => { + t = tspl(t, { plan: 2 }) + const server = createSecureServer(pem) server.on('stream', (stream, _headers, _flags, rawHeaders) => { - t.same(rawHeaders, [ + t.deepStrictEqual(rawHeaders, [ ':authority', `localhost:${server.address().port}`, ':method', @@ -1197,15 +1216,17 @@ test( allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', method: 'GET' }) - t.equal(response.statusCode, 200) + t.strictEqual(response.statusCode, 200) + + await t.complete } ) @@ -1229,9 +1250,9 @@ test('The h2 pseudo-headers is not included in the headers', async t => { allowH2: true }) - t.plan(2) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 2 }) + after(() => server.close()) + after(() => client.close()) const response = await client.request({ path: '/', @@ -1240,8 +1261,8 @@ test('The h2 pseudo-headers is not included in the headers', async t => { await response.body.text() - t.equal(response.statusCode, 200) - t.equal(response.headers[':status'], undefined) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers[':status'], undefined) }) test('Should throw informational error on half-closed streams (remote)', async t => { @@ -1261,15 +1282,15 @@ test('Should throw informational error on half-closed streams (remote)', async t allowH2: true }) - t.plan(2) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) + t = tspl(t, { plan: 2 }) + after(() => server.close()) + after(() => client.close()) await client.request({ path: '/', method: 'GET' }).catch(err => { - t.equal(err.message, 'HTTP/2: stream half-closed (remote)') - t.equal(err.code, 'UND_ERR_INFO') + t.strictEqual(err.message, 'HTTP/2: stream half-closed (remote)') + t.strictEqual(err.code, 'UND_ERR_INFO') }) }) diff --git a/test/readable.test.js b/test/readable.test.js index 3d6b5b1cea1..8e73301ea57 100644 --- a/test/readable.test.js +++ b/test/readable.test.js @@ -36,7 +36,7 @@ test('destroy timing text', async function (t) { const r = new Readable({ resume, abort }) r.destroy(new Error('kaboom')) - t.rejects(r.text(), new Error('kaboom')) + await t.rejects(r.text(), new Error('kaboom')) }) test('destroy timing promise', async function (t) { diff --git a/test/request.js b/test/request.js index bbfab5b3f5c..628bed0d415 100644 --- a/test/request.js +++ b/test/request.js @@ -1,10 +1,13 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') -const { test } = require('tap') +const { test, after, describe } = require('node:test') const { request, errors } = require('..') test('no-slash/one-slash pathname should be included in req.path', async (t) => { + t = tspl(t, { plan: 24 }) + const pathServer = createServer((req, res) => { t.fail('it shouldn\'t be called') res.statusCode = 200 @@ -12,15 +15,17 @@ test('no-slash/one-slash pathname should be included in req.path', async (t) => }) const requestedServer = createServer((req, res) => { - t.equal(`/localhost:${pathServer.address().port}`, req.url) - t.equal('GET', req.method) - t.equal(`localhost:${requestedServer.address().port}`, req.headers.host) + t.strictEqual(`/localhost:${pathServer.address().port}`, req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host) res.statusCode = 200 res.end('hello') }) - t.teardown(requestedServer.close.bind(requestedServer)) - t.teardown(pathServer.close.bind(pathServer)) + after(() => { + requestedServer.close() + pathServer.close() + }) await Promise.all([ requestedServer.listen(0), @@ -32,39 +37,41 @@ test('no-slash/one-slash pathname should be included in req.path', async (t) => origin: `http://localhost:${requestedServer.address().port}`, pathname: `localhost:${pathServer.address().port}` }) - t.equal(noSlashPathname.statusCode, 200) + t.strictEqual(noSlashPathname.statusCode, 200) const noSlashPath = await request({ method: 'GET', origin: `http://localhost:${requestedServer.address().port}`, path: `localhost:${pathServer.address().port}` }) - t.equal(noSlashPath.statusCode, 200) + t.strictEqual(noSlashPath.statusCode, 200) const noSlashPath2Arg = await request( `http://localhost:${requestedServer.address().port}`, { path: `localhost:${pathServer.address().port}` } ) - t.equal(noSlashPath2Arg.statusCode, 200) + t.strictEqual(noSlashPath2Arg.statusCode, 200) const oneSlashPathname = await request({ method: 'GET', origin: `http://localhost:${requestedServer.address().port}`, pathname: `/localhost:${pathServer.address().port}` }) - t.equal(oneSlashPathname.statusCode, 200) + t.strictEqual(oneSlashPathname.statusCode, 200) const oneSlashPath = await request({ method: 'GET', origin: `http://localhost:${requestedServer.address().port}`, path: `/localhost:${pathServer.address().port}` }) - t.equal(oneSlashPath.statusCode, 200) + t.strictEqual(oneSlashPath.statusCode, 200) const oneSlashPath2Arg = await request( `http://localhost:${requestedServer.address().port}`, { path: `/localhost:${pathServer.address().port}` } ) - t.equal(oneSlashPath2Arg.statusCode, 200) + t.strictEqual(oneSlashPath2Arg.statusCode, 200) t.end() }) test('protocol-relative URL as pathname should be included in req.path', async (t) => { + t = tspl(t, { plan: 12 }) + const pathServer = createServer((req, res) => { t.fail('it shouldn\'t be called') res.statusCode = 200 @@ -72,15 +79,17 @@ test('protocol-relative URL as pathname should be included in req.path', async ( }) const requestedServer = createServer((req, res) => { - t.equal(`//localhost:${pathServer.address().port}`, req.url) - t.equal('GET', req.method) - t.equal(`localhost:${requestedServer.address().port}`, req.headers.host) + t.strictEqual(`//localhost:${pathServer.address().port}`, req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host) res.statusCode = 200 res.end('hello') }) - t.teardown(requestedServer.close.bind(requestedServer)) - t.teardown(pathServer.close.bind(pathServer)) + after(() => { + requestedServer.close() + pathServer.close() + }) await Promise.all([ requestedServer.listen(0), @@ -92,22 +101,24 @@ test('protocol-relative URL as pathname should be included in req.path', async ( origin: `http://localhost:${requestedServer.address().port}`, pathname: `//localhost:${pathServer.address().port}` }) - t.equal(noSlashPathname.statusCode, 200) + t.strictEqual(noSlashPathname.statusCode, 200) const noSlashPath = await request({ method: 'GET', origin: `http://localhost:${requestedServer.address().port}`, path: `//localhost:${pathServer.address().port}` }) - t.equal(noSlashPath.statusCode, 200) + t.strictEqual(noSlashPath.statusCode, 200) const noSlashPath2Arg = await request( `http://localhost:${requestedServer.address().port}`, { path: `//localhost:${pathServer.address().port}` } ) - t.equal(noSlashPath2Arg.statusCode, 200) + t.strictEqual(noSlashPath2Arg.statusCode, 200) t.end() }) test('Absolute URL as pathname should be included in req.path', async (t) => { + t = tspl(t, { plan: 12 }) + const pathServer = createServer((req, res) => { t.fail('it shouldn\'t be called') res.statusCode = 200 @@ -115,15 +126,17 @@ test('Absolute URL as pathname should be included in req.path', async (t) => { }) const requestedServer = createServer((req, res) => { - t.equal(`/http://localhost:${pathServer.address().port}`, req.url) - t.equal('GET', req.method) - t.equal(`localhost:${requestedServer.address().port}`, req.headers.host) + t.strictEqual(`/http://localhost:${pathServer.address().port}`, req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${requestedServer.address().port}`, req.headers.host) res.statusCode = 200 res.end('hello') }) - t.teardown(requestedServer.close.bind(requestedServer)) - t.teardown(pathServer.close.bind(pathServer)) + after(() => { + requestedServer.close() + pathServer.close() + }) await Promise.all([ requestedServer.listen(0), @@ -135,46 +148,46 @@ test('Absolute URL as pathname should be included in req.path', async (t) => { origin: `http://localhost:${requestedServer.address().port}`, pathname: `http://localhost:${pathServer.address().port}` }) - t.equal(noSlashPathname.statusCode, 200) + t.strictEqual(noSlashPathname.statusCode, 200) const noSlashPath = await request({ method: 'GET', origin: `http://localhost:${requestedServer.address().port}`, path: `http://localhost:${pathServer.address().port}` }) - t.equal(noSlashPath.statusCode, 200) + t.strictEqual(noSlashPath.statusCode, 200) const noSlashPath2Arg = await request( `http://localhost:${requestedServer.address().port}`, { path: `http://localhost:${pathServer.address().port}` } ) - t.equal(noSlashPath2Arg.statusCode, 200) + t.strictEqual(noSlashPath2Arg.statusCode, 200) t.end() }) -test('DispatchOptions#reset', scope => { - scope.plan(4) +describe('DispatchOptions#reset', () => { + test('Should throw if invalid reset option', async t => { + t = tspl(t, { plan: 1 }) - scope.test('Should throw if invalid reset option', t => { - t.plan(1) - - t.rejects(request({ + await t.rejects(request({ method: 'GET', origin: 'http://somehost.xyz', reset: 0 - }), 'invalid reset') + }), /invalid reset/) + + await t.completed }) - scope.test('Should include "connection:close" if reset true', async t => { + test('Should include "connection:close" if reset true', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.connection, 'close') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.connection, 'close') res.statusCode = 200 res.end('hello') }) - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -190,18 +203,18 @@ test('DispatchOptions#reset', scope => { }) }) - scope.test('Should include "connection:keep-alive" if reset false', async t => { + test('Should include "connection:keep-alive" if reset false', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.connection, 'keep-alive') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.connection, 'keep-alive') res.statusCode = 200 res.end('hello') }) - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -217,18 +230,18 @@ test('DispatchOptions#reset', scope => { }) }) - scope.test('Should react to manual set of "connection:close" header', async t => { + test('Should react to manual set of "connection:close" header', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.connection, 'close') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.connection, 'close') res.statusCode = 200 res.end('hello') }) - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -247,14 +260,14 @@ test('DispatchOptions#reset', scope => { }) }) -test('Should include headers from iterable objects', scope => { - scope.plan(4) +describe('Should include headers from iterable objects', scope => { + test('Should include headers built with Headers global object', async t => { + t = tspl(t, { plan: 3 }) - scope.test('Should include headers built with Headers global object', async t => { const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.hello, 'world') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.hello, 'world') res.statusCode = 200 res.end('hello') }) @@ -262,9 +275,7 @@ test('Should include headers from iterable objects', scope => { const headers = new Headers() headers.set('hello', 'world') - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -281,11 +292,13 @@ test('Should include headers from iterable objects', scope => { }) }) - scope.test('Should include headers built with Map', async t => { + test('Should include headers built with Map', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.hello, 'world') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.hello, 'world') res.statusCode = 200 res.end('hello') }) @@ -293,9 +306,7 @@ test('Should include headers from iterable objects', scope => { const headers = new Map() headers.set('hello', 'world') - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -312,11 +323,13 @@ test('Should include headers from iterable objects', scope => { }) }) - scope.test('Should include headers built with custom iterable object', async t => { + test('Should include headers built with custom iterable object', async t => { + t = tspl(t, { plan: 3 }) + const server = createServer((req, res) => { - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.hello, 'world') + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.hello, 'world') res.statusCode = 200 res.end('hello') }) @@ -327,9 +340,7 @@ test('Should include headers from iterable objects', scope => { } } - t.plan(3) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -346,7 +357,9 @@ test('Should include headers from iterable objects', scope => { }) }) - scope.test('Should throw error if headers iterable object does not yield key-value pairs', async t => { + test('Should throw error if headers iterable object does not yield key-value pairs', async t => { + t = tspl(t, { plan: 2 }) + const server = createServer((req, res) => { res.end('hello') }) @@ -357,9 +370,7 @@ test('Should include headers from iterable objects', scope => { } } - t.plan(2) - - t.teardown(server.close.bind(server)) + after(() => server.close()) await new Promise((resolve, reject) => { server.listen(0, (err) => { @@ -374,8 +385,8 @@ test('Should include headers from iterable objects', scope => { reset: true, headers }).catch((err) => { - t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'headers must be in key-value pair format') + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'headers must be in key-value pair format') }) }) }) From ae9587193403bccac4fdfe9ebc882c5240538cf3 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 12 Feb 2024 16:02:45 +0100 Subject: [PATCH 024/123] chore: migrate a batch of tests to node test runner (#2742) --- test/client-errors.js | 15 +- test/client-stream.js | 435 ++++++++++++++++++++---------------- test/connect-abort.js | 13 +- test/http-req-destroy.js | 23 +- test/https.js | 47 ++-- test/max-response-size.js | 57 ++--- test/parser-issues.js | 51 +++-- test/pipeline-pipelining.js | 45 ++-- test/socket-timeout.js | 45 ++-- test/stream-compat.js | 31 +-- test/trailers.js | 31 +-- 11 files changed, 446 insertions(+), 347 deletions(-) diff --git a/test/client-errors.js b/test/client-errors.js index 21732ac075a..0c935d7cdac 100644 --- a/test/client-errors.js +++ b/test/client-errors.js @@ -1,28 +1,31 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const net = require('node:net') // TODO: move to test/node-test/client-connect.js -test('parser error', (t) => { - t.plan(2) +test('parser error', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer() server.once('connection', (socket) => { socket.write('asd\n\r213123') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err) => { t.ok(err) client.close((err) => { - t.error(err) + t.ifError(err) }) }) }) + + await t.completed }) diff --git a/test/client-stream.js b/test/client-stream.js index 69843acf0df..8df8c690aea 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -1,26 +1,27 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') const { PassThrough, Writable, Readable } = require('node:stream') const EE = require('node:events') -test('stream get', (t) => { - t.plan(9) +test('stream get', async (t) => { + t = tspl(t, { plan: 9 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.stream({ @@ -29,81 +30,85 @@ test('stream get', (t) => { method: 'GET', opaque: new PassThrough() }, ({ statusCode, headers, opaque: pt }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] pt.on('data', (buf) => { bufs.push(buf) }) pt.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) return pt }, (err) => { - t.equal(signal.listenerCount('abort'), 0) - t.error(err) + t.strictEqual(signal.listenerCount('abort'), 0) + t.ifError(err) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('stream promise get', (t) => { - t.plan(6) +test('stream promise get', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) await client.stream({ path: '/', method: 'GET', opaque: new PassThrough() }, ({ statusCode, headers, opaque: pt }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] pt.on('data', (buf) => { bufs.push(buf) }) pt.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) return pt }) }) + + await t.completed }) -test('stream GET destroy res', (t) => { - t.plan(14) +test('stream GET destroy res', async (t) => { + t = tspl(t, { plan: 14 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.stream({ path: '/', method: 'GET' }, ({ statusCode, headers }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const pt = new PassThrough() .on('error', (err) => { @@ -122,26 +127,28 @@ test('stream GET destroy res', (t) => { path: '/', method: 'GET' }, ({ statusCode, headers }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') let ret = '' const pt = new PassThrough() pt.on('data', chunk => { ret += chunk }).on('end', () => { - t.equal(ret, 'hello') + t.strictEqual(ret, 'hello') }) return pt }, (err) => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('stream GET remote destroy', (t) => { - t.plan(4) +test('stream GET remote destroy', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.write('asd') @@ -149,11 +156,11 @@ test('stream GET remote destroy', (t) => { res.destroy() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.stream({ path: '/', @@ -181,10 +188,12 @@ test('stream GET remote destroy', (t) => { t.ok(err) }) }) + + await t.completed }) -test('stream response resume back pressure and non standard error', (t) => { - t.plan(5) +test('stream response resume back pressure and non standard error', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { res.write(Buffer.alloc(1e3)) @@ -193,11 +202,11 @@ test('stream response resume back pressure and non standard error', (t) => { res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const pt = new PassThrough() client.stream({ @@ -207,12 +216,12 @@ test('stream response resume back pressure and non standard error', (t) => { pt.on('data', () => { pt.emit('error', new Error('kaboom')) }).once('error', (err) => { - t.equal(err.message, 'kaboom') + t.strictEqual(err.message, 'kaboom') }) return pt }, (err) => { t.ok(err) - t.equal(pt.destroyed, true) + t.strictEqual(pt.destroyed, true) }) client.once('disconnect', (err) => { @@ -227,36 +236,40 @@ test('stream response resume back pressure and non standard error', (t) => { pt.resume() return pt }, (err) => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('stream waits only for writable side', (t) => { - t.plan(2) +test('stream waits only for writable side', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end(Buffer.alloc(1e3)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const pt = new PassThrough({ autoDestroy: false }) client.stream({ path: '/', method: 'GET' }, () => pt, (err) => { - t.error(err) - t.equal(pt.destroyed, false) + t.ifError(err) + t.strictEqual(pt.destroyed, false) }) }) + + await t.completed }) -test('stream args validation', (t) => { - t.plan(3) +test('stream args validation', async (t) => { + t = tspl(t, { plan: 3 }) const client = new Client('http://localhost:5000') client.stream({ @@ -277,8 +290,8 @@ test('stream args validation', (t) => { } }) -test('stream args validation promise', (t) => { - t.plan(2) +test('stream args validation promise', async (t) => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:5000') client.stream({ @@ -291,21 +304,23 @@ test('stream args validation promise', (t) => { client.stream(null, null).catch((err) => { t.ok(err instanceof errors.InvalidArgumentError) }) + + await t.completed }) -test('stream destroy if not readable', (t) => { - t.plan(2) +test('stream destroy if not readable', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const pt = new PassThrough() pt.readable = false server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -313,23 +328,25 @@ test('stream destroy if not readable', (t) => { }, () => { return pt }, (err) => { - t.error(err) - t.equal(pt.destroyed, true) + t.ifError(err) + t.strictEqual(pt.destroyed, true) }) }) + + await t.completed }) -test('stream server side destroy', (t) => { - t.plan(1) +test('stream server side destroy', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -340,19 +357,21 @@ test('stream server side destroy', (t) => { t.ok(err instanceof errors.SocketError) }) }) + + await t.completed }) -test('stream invalid return', (t) => { - t.plan(1) +test('stream invalid return', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.write('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -363,19 +382,21 @@ test('stream invalid return', (t) => { t.ok(err instanceof errors.InvalidReturnValueError) }) }) + + await t.completed }) -test('stream body without destroy', (t) => { - t.plan(1) +test('stream body without destroy', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -386,22 +407,24 @@ test('stream body without destroy', (t) => { pt.resume() return pt }, (err) => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('stream factory abort', (t) => { - t.plan(3) +test('stream factory abort', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) const signal = new EE() client.stream({ @@ -412,24 +435,26 @@ test('stream factory abort', (t) => { signal.emit('abort') return new PassThrough() }, (err) => { - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) t.ok(err instanceof errors.RequestAbortedError) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('stream factory throw', (t) => { - t.plan(3) +test('stream factory throw', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -437,7 +462,7 @@ test('stream factory throw', (t) => { }, () => { throw new Error('asd') }, (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) client.stream({ path: '/', @@ -445,7 +470,7 @@ test('stream factory throw', (t) => { }, () => { throw new Error('asd') }, (err) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) client.stream({ path: '/', @@ -453,22 +478,24 @@ test('stream factory throw', (t) => { }, () => { return new PassThrough() }, (err) => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('stream CONNECT throw', (t) => { - t.plan(1) +test('stream CONNECT throw', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) client.stream({ path: '/', @@ -478,19 +505,21 @@ test('stream CONNECT throw', (t) => { t.ok(err instanceof errors.InvalidArgumentError) }) }) + + await t.completed }) -test('stream abort after complete', (t) => { - t.plan(1) +test('stream abort after complete', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) const pt = new PassThrough() const signal = new EE() @@ -501,23 +530,25 @@ test('stream abort after complete', (t) => { }, () => { return pt }, (err) => { - t.error(err) + t.ifError(err) signal.emit('abort') }) }) + + await t.completed }) -test('stream abort before dispatch', (t) => { - t.plan(1) +test('stream abort before dispatch', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) const pt = new PassThrough() const signal = new EE() @@ -532,44 +563,48 @@ test('stream abort before dispatch', (t) => { }) signal.emit('abort') }) + + await t.completed }) -test('trailers', (t) => { - t.plan(2) +test('trailers', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.writeHead(200, { Trailer: 'Content-MD5' }) res.addTrailers({ 'Content-MD5': 'test' }) res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.stream({ path: '/', method: 'GET' }, () => new PassThrough(), (err, data) => { - t.error(err) - t.strictSame(data.trailers, { 'content-md5': 'test' }) + t.ifError(err) + t.deepStrictEqual(data.trailers, { 'content-md5': 'test' }) }) }) + + await t.completed }) -test('stream ignore 1xx', (t) => { - t.plan(2) +test('stream ignore 1xx', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.writeProcessing() res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.stream({ @@ -581,25 +616,27 @@ test('stream ignore 1xx', (t) => { callback() } }), (err, data) => { - t.error(err) - t.equal(buf, 'hello') + t.ifError(err) + t.strictEqual(buf, 'hello') }) }) + + await t.completed }) -test('stream ignore 1xx and use onInfo', (t) => { - t.plan(4) +test('stream ignore 1xx and use onInfo', async (t) => { + t = tspl(t, { plan: 4 }) const infos = [] const server = createServer((req, res) => { res.writeProcessing() res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.stream({ @@ -614,16 +651,18 @@ test('stream ignore 1xx and use onInfo', (t) => { callback() } }), (err, data) => { - t.error(err) - t.equal(buf, 'hello') - t.equal(infos.length, 1) - t.equal(infos[0].statusCode, 102) + t.ifError(err) + t.strictEqual(buf, 'hello') + t.strictEqual(infos.length, 1) + t.strictEqual(infos[0].statusCode, 102) }) }) + + await t.completed }) -test('stream backpressure', (t) => { - t.plan(2) +test('stream backpressure', async (t) => { + t = tspl(t, { plan: 2 }) const expected = Buffer.alloc(1e6).toString() @@ -631,11 +670,11 @@ test('stream backpressure', (t) => { res.writeProcessing() res.end(expected) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.stream({ @@ -648,49 +687,53 @@ test('stream backpressure', (t) => { process.nextTick(callback) } }), (err, data) => { - t.error(err) - t.equal(buf, expected) + t.ifError(err) + t.strictEqual(buf, expected) }) }) + + await t.completed }) -test('stream body destroyed on invalid callback', (t) => { - t.plan(1) +test('stream body destroyed on invalid callback', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(client.destroy.bind(client)) const body = new Readable({ - read () {} + read () { } }) try { client.stream({ path: '/', method: 'GET', body - }, () => {}, null) + }, () => { }, null) } catch (err) { - t.equal(body.destroyed, true) + t.strictEqual(body.destroyed, true) } }) + + await t.completed }) -test('stream needDrain', (t) => { - t.plan(3) +test('stream needDrain', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end(Buffer.alloc(4096)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(() => { + after(() => { client.destroy() }) @@ -715,8 +758,8 @@ test('stream needDrain', (t) => { path: '/', method: 'GET' }, () => { - t.equal(dst._writableState.needDrain, true) - t.equal(dst.writableNeedDrain, true) + t.strictEqual(dst._writableState.needDrain, true) + t.strictEqual(dst.writableNeedDrain, true) setImmediate(() => { dst.write = (...args) => { @@ -732,19 +775,21 @@ test('stream needDrain', (t) => { t.ok(true, 'pass') }) }) + + await t.completed }) -test('stream legacy needDrain', (t) => { - t.plan(3) +test('stream legacy needDrain', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end(Buffer.alloc(4096)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(() => { + after(() => { client.destroy() }) @@ -768,8 +813,8 @@ test('stream legacy needDrain', (t) => { path: '/', method: 'GET' }, () => { - t.equal(dst._writableState.needDrain, true) - t.equal(dst.writableNeedDrain, undefined) + t.strictEqual(dst._writableState.needDrain, true) + t.strictEqual(dst.writableNeedDrain, undefined) setImmediate(() => { dst.write = (...args) => { @@ -785,63 +830,67 @@ test('stream legacy needDrain', (t) => { t.ok(true, 'pass') }) }) + await t.completed +}) - test('stream throwOnError', (t) => { - t.plan(2) +test('stream throwOnError', async (t) => { + t = tspl(t, { plan: 3 }) - const errStatusCode = 500 - const errMessage = 'Internal Server Error' + const errStatusCode = 500 + const errMessage = 'Internal Server Error' - const server = createServer((req, res) => { - res.writeHead(errStatusCode, { 'Content-Type': 'text/plain' }) - res.end(errMessage) - }) - t.teardown(server.close.bind(server)) + const server = createServer((req, res) => { + res.writeHead(errStatusCode, { 'Content-Type': 'text/plain' }) + res.end(errMessage) + }) + after(() => server.close()) - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) - client.stream({ - path: '/', - method: 'GET', - throwOnError: true, - opaque: new PassThrough() - }, ({ opaque: pt }) => { - pt.on('data', () => { - t.fail() - }) - return pt - }, (e) => { - t.equal(e.status, errStatusCode) - t.equal(e.body, errMessage) - t.end() + client.stream({ + path: '/', + method: 'GET', + throwOnError: true, + opaque: new PassThrough() + }, ({ opaque: pt }) => { + pt.on('data', () => { + t.fail() }) + return pt + }, (e) => { + t.strictEqual(e.status, errStatusCode) + t.strictEqual(e.body, errMessage) + t.ok(true, 'end') }) }) - test('steam throwOnError=true, error on stream', (t) => { - t.plan(1) + await t.completed +}) - const server = createServer((req, res) => { - res.end('asd') - }) - t.teardown(server.close.bind(server)) +test('steam throwOnError=true, error on stream', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = createServer((req, res) => { + res.end('asd') + }) + after(() => server.close()) - server.listen(0, async () => { - const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) - client.stream({ - path: '/', - method: 'GET', - throwOnError: true, - opaque: new PassThrough() - }, () => { - throw new Error('asd') - }, (e) => { - t.equal(e.message, 'asd') - }) + client.stream({ + path: '/', + method: 'GET', + throwOnError: true, + opaque: new PassThrough() + }, () => { + throw new Error('asd') + }, (e) => { + t.strictEqual(e.message, 'asd') }) }) + await t.completed }) diff --git a/test/connect-abort.js b/test/connect-abort.js index 91ed1b2a258..bf75fd8bd6a 100644 --- a/test/connect-abort.js +++ b/test/connect-abort.js @@ -1,18 +1,19 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') const { Client } = require('..') const { PassThrough } = require('node:stream') -test(t => { - t.plan(2) +test('connect-abort', async t => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:1234', { connect: (_, cb) => { client.destroy() cb(null, new PassThrough({ destroy (err, cb) { - t.same(err?.name, 'ClientDestroyedError') + t.strictEqual(err.name, 'ClientDestroyedError') cb(null) } })) @@ -23,6 +24,8 @@ test(t => { path: '/', method: 'GET' }, (err, data) => { - t.same(err?.name, 'ClientDestroyedError') + t.strictEqual(err.name, 'ClientDestroyedError') }) + + await t.completed }) diff --git a/test/http-req-destroy.js b/test/http-req-destroy.js index d0d83b6d848..ea7624c611d 100644 --- a/test/http-req-destroy.js +++ b/test/http-req-destroy.js @@ -1,24 +1,25 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const undici = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const { maybeWrapStream, consts } = require('./utils/async-iterators') function doNotKillReqSocket (bodyType) { - test(`do not kill req socket ${bodyType}`, (t) => { - t.plan(3) + test(`do not kill req socket ${bodyType}`, async (t) => { + t = tspl(t, { plan: 3 }) const server1 = createServer((req, res) => { const client = new undici.Client(`http://localhost:${server2.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'POST', body: req }, (err, response) => { - t.error(err) + t.ifError(err) setTimeout(() => { response.body.on('data', buf => { res.write(buf) @@ -29,18 +30,18 @@ function doNotKillReqSocket (bodyType) { }, 100) }) }) - t.teardown(server1.close.bind(server1)) + after(() => server1.close()) const server2 = createServer((req, res) => { setTimeout(() => { req.pipe(res) }, 100) }) - t.teardown(server2.close.bind(server2)) + after(() => server2.close()) server1.listen(0, () => { const client = new undici.Client(`http://localhost:${server1.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const r = new Readable({ read () {} }) r.push('hello') @@ -49,19 +50,21 @@ function doNotKillReqSocket (bodyType) { method: 'POST', body: maybeWrapStream(r, bodyType) }, (err, response) => { - t.error(err) + t.ifError(err) const bufs = [] response.body.on('data', (buf) => { bufs.push(buf) r.push(null) }) response.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) server2.listen(0) + + await t.completed }) } diff --git a/test/https.js b/test/https.js index 5d9ab3d8e22..418fb969f45 100644 --- a/test/https.js +++ b/test/https.js @@ -1,20 +1,21 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') const pem = require('https-pem') -test('https get with tls opts', (t) => { - t.plan(6) +test('https get with tls opts', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer(pem, (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`https://localhost:${server.address().port}`, { @@ -22,33 +23,35 @@ test('https get with tls opts', (t) => { rejectUnauthorized: false } }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('https get with tls opts ip', (t) => { - t.plan(6) +test('https get with tls opts ip', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer(pem, (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`https://127.0.0.1:${server.address().port}`, { @@ -56,19 +59,21 @@ test('https get with tls opts ip', (t) => { rejectUnauthorized: false } }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) diff --git a/test/max-response-size.js b/test/max-response-size.js index adf75c6fee2..1e0d904469a 100644 --- a/test/max-response-size.js +++ b/test/max-response-size.js @@ -1,17 +1,16 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') -test('max response size', (t) => { - t.plan(4) - - t.test('default max default size should allow all responses', (t) => { - t.plan(3) +describe('max response size', async (t) => { + test('default max default size should allow all responses', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer() - t.teardown(server.close.bind(server)) + after(() => server.close()) server.on('request', (req, res) => { res.end('hello') @@ -19,27 +18,29 @@ test('max response size', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: -1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) - t.test('max response size set to zero should allow only empty responses', (t) => { - t.plan(3) + test('max response size set to zero should allow only empty responses', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer() - t.teardown(server.close.bind(server)) + after(() => server.close()) server.on('request', (req, res) => { res.end() @@ -47,27 +48,29 @@ test('max response size', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { maxResponseSize: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) - t.test('should throw an error if the response is too big', (t) => { - t.plan(3) + test('should throw an error if the response is too big', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer() - t.teardown(server.close.bind(server)) + after(() => server.close()) server.on('request', (req, res) => { res.end('hello') @@ -78,20 +81,22 @@ test('max response size', (t) => { maxResponseSize: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.on('error', (err) => { t.ok(err) t.ok(err instanceof errors.ResponseExceededMaxSizeError) }) }) }) + + await t.completed }) - t.test('invalid max response size should throw an error', (t) => { - t.plan(2) + test('invalid max response size should throw an error', async (t) => { + t = tspl(t, { plan: 2 }) t.throws(() => { // eslint-disable-next-line no-new @@ -102,4 +107,6 @@ test('max response size', (t) => { new Client('http://localhost:3000', { maxResponseSize: -2 }) }, 'maxResponseSize must be greater than or equal to -1') }) + + await t.completed }) diff --git a/test/parser-issues.js b/test/parser-issues.js index 3602ae1ba5e..2d9f04628de 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -1,9 +1,12 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const net = require('node:net') -const { test } = require('tap') const { Client, errors } = require('..') -test('https://github.com/mcollina/undici/issues/268', (t) => { - t.plan(2) +test('https://github.com/mcollina/undici/issues/268', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\n') @@ -17,18 +20,18 @@ test('https://github.com/mcollina/undici/issues/268', (t) => { }, 500) }, 500) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ method: 'GET', path: '/nxt/_changes?feed=continuous&heartbeat=5000', headersTimeout: 1e3 }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() setTimeout(() => { @@ -37,19 +40,21 @@ test('https://github.com/mcollina/undici/issues/268', (t) => { }, 2e3) }) }) + + await t.completed }) -test('parser fail', (t) => { - t.plan(2) +test('parser fail', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTT/1.1 200 OK\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ method: 'GET', @@ -59,10 +64,12 @@ test('parser fail', (t) => { t.ok(err instanceof errors.HTTPParserError) }) }) + + await t.completed }) -test('split header field', (t) => { - t.plan(2) +test('split header field', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\nA') @@ -70,25 +77,27 @@ test('split header field', (t) => { socket.write('SD: asd,asd\r\n\r\n\r\n') }, 100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ method: 'GET', path: '/' }, (err, data) => { - t.error(err) + t.ifError(err) t.equal(data.headers.asd, 'asd,asd') data.body.destroy().on('error', () => {}) }) }) + + await t.completed }) -test('split header value', (t) => { - t.plan(2) +test('split header value', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer(socket => { socket.write('HTTP/1.1 200 OK\r\nASD: asd') @@ -96,19 +105,21 @@ test('split header value', (t) => { socket.write(',asd\r\n\r\n\r\n') }, 100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ method: 'GET', path: '/' }, (err, data) => { - t.error(err) + t.ifError(err) t.equal(data.headers.asd, 'asd,asd') data.body.destroy().on('error', () => {}) }) }) + + await t.completed }) diff --git a/test/pipeline-pipelining.js b/test/pipeline-pipelining.js index d3f7143cba4..b244925786a 100644 --- a/test/pipeline-pipelining.js +++ b/test/pipeline-pipelining.js @@ -1,25 +1,26 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { kConnect } = require('../lib/core/symbols') const { kBusy, kPending, kRunning } = require('../lib/core/symbols') -test('pipeline pipelining', (t) => { - t.plan(10) +test('pipeline pipelining', async (t) => { + t = tspl(t, { plan: 10 }) const server = createServer((req, res) => { - t.strictSame(req.headers['transfer-encoding'], undefined) + t.deepStrictEqual(req.headers['transfer-encoding'], undefined) res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client[kConnect](() => { t.equal(client[kRunning], 0) @@ -28,25 +29,27 @@ test('pipeline pipelining', (t) => { path: '/' }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) - t.strictSame(client[kRunning], 0) - t.strictSame(client[kPending], 1) + t.deepStrictEqual(client[kRunning], 0) + t.deepStrictEqual(client[kPending], 1) client.pipeline({ method: 'GET', path: '/' }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) - t.strictSame(client[kRunning], 0) - t.strictSame(client[kPending], 2) + t.deepStrictEqual(client[kRunning], 0) + t.deepStrictEqual(client[kPending], 2) process.nextTick(() => { t.equal(client[kRunning], 2) }) }) }) + + await t.completed }) -test('pipeline pipelining retry', (t) => { - t.plan(13) +test('pipeline pipelining retry', async (t) => { + t = tspl(t, { plan: 13 }) let count = 0 const server = createServer((req, res) => { @@ -57,12 +60,12 @@ test('pipeline pipelining retry', (t) => { } }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.once('disconnect', () => { t.ok(true, 'pass') @@ -77,24 +80,24 @@ test('pipeline pipelining retry', (t) => { t.ok(err) }) t.equal(client[kBusy], true) - t.strictSame(client[kRunning], 0) - t.strictSame(client[kPending], 1) + t.deepStrictEqual(client[kRunning], 0) + t.deepStrictEqual(client[kPending], 1) client.pipeline({ method: 'GET', path: '/' }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) - t.strictSame(client[kRunning], 0) - t.strictSame(client[kPending], 2) + t.deepStrictEqual(client[kRunning], 0) + t.deepStrictEqual(client[kPending], 2) client.pipeline({ method: 'GET', path: '/' }, ({ body }) => body).end().resume() t.equal(client[kBusy], true) - t.strictSame(client[kRunning], 0) - t.strictSame(client[kPending], 3) + t.deepStrictEqual(client[kRunning], 0) + t.deepStrictEqual(client[kPending], 3) process.nextTick(() => { t.equal(client[kRunning], 3) @@ -105,4 +108,6 @@ test('pipeline pipelining retry', (t) => { }) }) }) + + await t.completed }) diff --git a/test/socket-timeout.js b/test/socket-timeout.js index a0facda8369..83617e94f7e 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -1,13 +1,14 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const timers = require('../lib/timers') const { createServer } = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') -test('timeout with pipelining 1', (t) => { - t.plan(9) +test('timeout with pipelining 1', async (t) => { + t = tspl(t, { plan: 9 }) const server = createServer() @@ -15,13 +16,13 @@ test('timeout with pipelining 1', (t) => { t.ok(true, 'first request received, we are letting this timeout on the client') server.once('request', (req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { @@ -29,7 +30,7 @@ test('timeout with pipelining 1', (t) => { headersTimeout: 500, bodyTimeout: 500 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -37,37 +38,39 @@ test('timeout with pipelining 1', (t) => { opaque: 'asd' }, (err, data) => { t.ok(err instanceof errors.HeadersTimeoutError) // we are expecting an error - t.equal(data.opaque, 'asd') + t.strictEqual(data.opaque, 'asd') }) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('Disable socket timeout', (t) => { - t.plan(2) +test('Disable socket timeout', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer() const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + after(clock.uninstall.bind(clock)) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -77,24 +80,26 @@ test('Disable socket timeout', (t) => { }, 31e3) clock.tick(32e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0, headersTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, result) => { - t.error(err) + t.ifError(err) const bufs = [] result.body.on('data', (buf) => { bufs.push(buf) }) result.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) diff --git a/test/stream-compat.js b/test/stream-compat.js index 02d521a9d65..5f219fe42e3 100644 --- a/test/stream-compat.js +++ b/test/stream-compat.js @@ -1,21 +1,22 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const EE = require('node:events') -test('stream body without destroy', (t) => { - t.plan(2) +test('stream body without destroy', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const signal = new EE() const body = new Readable({ read () {} }) @@ -33,19 +34,21 @@ test('stream body without destroy', (t) => { }) signal.emit('abort') }) + + await t.completed }) -test('IncomingMessage', (t) => { - t.plan(2) +test('IncomingMessage', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const proxyClient = new Client(`http://localhost:${server.address().port}`) - t.teardown(proxyClient.destroy.bind(proxyClient)) + after(() => proxyClient.destroy()) const proxy = createServer((req, res) => { proxyClient.request({ @@ -53,23 +56,25 @@ test('IncomingMessage', (t) => { method: 'PUT', body: req }, (err, data) => { - t.error(err) + t.ifError(err) data.body.pipe(res) }) }) - t.teardown(proxy.close.bind(proxy)) + after(() => proxy.close()) proxy.listen(0, () => { const client = new Client(`http://localhost:${proxy.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'PUT', body: 'hello world' }, (err, data) => { - t.error(err) + t.ifError(err) }) }) }) + + await t.completed }) diff --git a/test/trailers.js b/test/trailers.js index e56542568ab..8f616e87eba 100644 --- a/test/trailers.js +++ b/test/trailers.js @@ -1,11 +1,12 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') -test('response trailers missing is OK', (t) => { - t.plan(1) +test('response trailers missing is OK', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.writeHead(200, { @@ -13,23 +14,24 @@ test('response trailers missing is OK', (t) => { }) res.end('response') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - + after(() => client.destroy()) const { body } = await client.request({ path: '/', method: 'GET', body: 'asd' }) - t.equal(await body.text(), 'response') + t.strictEqual(await body.text(), 'response') }) + + await t.completed }) -test('response trailers missing w trailers is OK', (t) => { - t.plan(2) +test('response trailers missing w trailers is OK', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.writeHead(200, { @@ -40,18 +42,19 @@ test('response trailers missing w trailers is OK', (t) => { }) res.end('response') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) - + after(() => client.destroy()) const { body, trailers } = await client.request({ path: '/', method: 'GET', body: 'asd' }) - t.equal(await body.text(), 'response') - t.same(trailers, { asd: 'foo' }) + t.strictEqual(await body.text(), 'response') + t.deepStrictEqual(trailers, { asd: 'foo' }) }) + + await t.completed }) From e17a1639be68d5a1edba08cb1f5a0e53d3c16c12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 22:51:32 +0000 Subject: [PATCH 025/123] build(deps-dev): bump cronometro from 2.0.2 to 3.0.1 (#2749) Bumps [cronometro](https://github.com/ShogunPanda/cronometro) from 2.0.2 to 3.0.1. - [Release notes](https://github.com/ShogunPanda/cronometro/releases) - [Changelog](https://github.com/ShogunPanda/cronometro/blob/main/CHANGELOG.md) - [Commits](https://github.com/ShogunPanda/cronometro/compare/v2.0.2...v3.0.1) --- updated-dependencies: - dependency-name: cronometro dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index acbb493382d..92227f2799e 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "axios": "^1.6.5", "borp": "^0.9.1", "concurrently": "^8.0.1", - "cronometro": "^2.0.2", + "cronometro": "^3.0.1", "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", "form-data": "^4.0.0", From c60acb50353e13ab2d14f4be82071028590a085c Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:20:34 +0900 Subject: [PATCH 026/123] perf: always use the same prototype Iterator (#2743) * perf: always use the same prototype Iterator * add iteratorMixin * rename state to object * fixup * perf: use class make faster * fixup * fixup * fixup * fixup * fixup * add test * fix test name * simplify --- lib/fetch/formdata.js | 62 +-------------- lib/fetch/headers.js | 62 +-------------- lib/fetch/util.js | 171 ++++++++++++++++++++++++++++++++---------- lib/fetch/webidl.js | 2 +- test/fetch/headers.js | 15 ++++ 5 files changed, 152 insertions(+), 160 deletions(-) diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index add64aa9226..80df2b8f399 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -1,6 +1,6 @@ 'use strict' -const { isBlobLike, makeIterator } = require('./util') +const { isBlobLike, iteratorMixin } = require('./util') const { kState } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { File: UndiciFile, FileLike, isFileLike } = require('./file') @@ -154,62 +154,9 @@ class FormData { this[kState].push(entry) } } - - entries () { - webidl.brandCheck(this, FormData) - - return makeIterator( - () => this[kState], - 'FormData', - 'key+value', - 'name', 'value' - ) - } - - keys () { - webidl.brandCheck(this, FormData) - - return makeIterator( - () => this[kState], - 'FormData', - 'key', - 'name', 'value' - ) - } - - values () { - webidl.brandCheck(this, FormData) - - return makeIterator( - () => this[kState], - 'FormData', - 'value', - 'name', 'value' - ) - } - - /** - * @param {(value: string, key: string, self: FormData) => void} callbackFn - * @param {unknown} thisArg - */ - forEach (callbackFn, thisArg = globalThis) { - webidl.brandCheck(this, FormData) - - webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) - - if (typeof callbackFn !== 'function') { - throw new TypeError( - "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." - ) - } - - for (const [key, value] of this) { - callbackFn.call(thisArg, value, key, this) - } - } } -FormData.prototype[Symbol.iterator] = FormData.prototype.entries +iteratorMixin('FormData', FormData, kState, 'name', 'value') Object.defineProperties(FormData.prototype, { append: kEnumerableProperty, @@ -218,11 +165,6 @@ Object.defineProperties(FormData.prototype, { getAll: kEnumerableProperty, has: kEnumerableProperty, set: kEnumerableProperty, - entries: kEnumerableProperty, - keys: kEnumerableProperty, - values: kEnumerableProperty, - forEach: kEnumerableProperty, - [Symbol.iterator]: { enumerable: false }, [Symbol.toStringTag]: { value: 'FormData', configurable: true diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 504942edc6e..43860c5d98a 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -6,7 +6,7 @@ const { kHeadersList, kConstruct } = require('../core/symbols') const { kGuard } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { - makeIterator, + iteratorMixin, isValidHeaderName, isValidHeaderValue } = require('./util') @@ -504,59 +504,6 @@ class Headers { return headers } - keys () { - webidl.brandCheck(this, Headers) - - return makeIterator( - () => this[kHeadersSortedMap], - 'Headers', - 'key', - 0, 1 - ) - } - - values () { - webidl.brandCheck(this, Headers) - - return makeIterator( - () => this[kHeadersSortedMap], - 'Headers', - 'value', - 0, 1 - ) - } - - entries () { - webidl.brandCheck(this, Headers) - - return makeIterator( - () => this[kHeadersSortedMap], - 'Headers', - 'key+value', - 0, 1 - ) - } - - /** - * @param {(value: string, key: string, self: Headers) => void} callbackFn - * @param {unknown} thisArg - */ - forEach (callbackFn, thisArg = globalThis) { - webidl.brandCheck(this, Headers) - - webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) - - if (typeof callbackFn !== 'function') { - throw new TypeError( - "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." - ) - } - - for (const [key, value] of this) { - callbackFn.call(thisArg, value, key, this) - } - } - [Symbol.for('nodejs.util.inspect.custom')] () { webidl.brandCheck(this, Headers) @@ -564,7 +511,7 @@ class Headers { } } -Headers.prototype[Symbol.iterator] = Headers.prototype.entries +iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1) Object.defineProperties(Headers.prototype, { append: kEnumerableProperty, @@ -573,11 +520,6 @@ Object.defineProperties(Headers.prototype, { has: kEnumerableProperty, set: kEnumerableProperty, getSetCookie: kEnumerableProperty, - keys: kEnumerableProperty, - values: kEnumerableProperty, - entries: kEnumerableProperty, - forEach: kEnumerableProperty, - [Symbol.iterator]: { enumerable: false }, [Symbol.toStringTag]: { value: 'Headers', configurable: true diff --git a/lib/fetch/util.js b/lib/fetch/util.js index c5a6b46b170..82e96ec9acd 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -6,9 +6,10 @@ const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet const { getGlobalOrigin } = require('./global') const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./dataURL') const { performance } = require('node:perf_hooks') -const { isBlobLike, toUSVString, ReadableStreamFrom, isValidHTTPToken } = require('../core/util') +const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../core/util') const assert = require('node:assert') const { isUint8Array } = require('util/types') +const { webidl } = require('./webidl') // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable /** @type {import('crypto')|undefined} */ @@ -739,35 +740,40 @@ const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbo /** * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object - * @param {() => unknown} iterator * @param {string} name name of the instance - * @param {'key'|'value'|'key+value'} kind + * @param {symbol} kInternalIterator * @param {string | number} [keyIndex] * @param {string | number} [valueIndex] */ -function makeIterator (iterator, name, kind, keyIndex = 0, valueIndex = 1) { - const object = { - index: 0, - kind, - target: iterator - } - // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. - const iteratorObject = Object.create(esIteratorPrototype) +function createIterator (name, kInternalIterator, keyIndex = 0, valueIndex = 1) { + class FastIterableIterator { + /** @type {any} */ + #target + /** @type {'key' | 'value' | 'key+value'} */ + #kind + /** @type {number} */ + #index + + /** + * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object + * @param {unknown} target + * @param {'key' | 'value' | 'key+value'} kind + */ + constructor (target, kind) { + this.#target = target + this.#kind = kind + this.#index = 0 + } - Object.defineProperty(iteratorObject, 'next', { - value: function next () { + next () { // 1. Let interface be the interface for which the iterator prototype object exists. - // 2. Let thisValue be the this value. - // 3. Let object be ? ToObject(thisValue). - // 4. If object is a platform object, then perform a security // check, passing: - // 5. If object is not a default iterator object for interface, // then throw a TypeError. - if (Object.getPrototypeOf(this) !== iteratorObject) { + if (typeof this !== 'object' || this === null || !(#target in this)) { throw new TypeError( `'next' called on an object that does not implement interface ${name} Iterator.` ) @@ -776,8 +782,8 @@ function makeIterator (iterator, name, kind, keyIndex = 0, valueIndex = 1) { // 6. Let index be object’s index. // 7. Let kind be object’s kind. // 8. Let values be object’s target's value pairs to iterate over. - const { index, kind, target } = object - const values = target() + const index = this.#index + const values = this.#target[kInternalIterator] // 9. Let len be the length of values. const len = values.length @@ -785,17 +791,25 @@ function makeIterator (iterator, name, kind, keyIndex = 0, valueIndex = 1) { // 10. If index is greater than or equal to len, then return // CreateIterResultObject(undefined, true). if (index >= len) { - return { value: undefined, done: true } + return { + value: undefined, + done: true + } } + // 11. Let pair be the entry in values at index index. const { [keyIndex]: key, [valueIndex]: value } = values[index] + // 12. Set object’s index to index + 1. - object.index = index + 1 + this.#index = index + 1 + // 13. Return the iterator result for pair and kind. + // https://webidl.spec.whatwg.org/#iterator-result + // 1. Let result be a value determined by the value of kind: let result - switch (kind) { + switch (this.#kind) { case 'key': // 1. Let idlKey be pair’s key. // 2. Let key be the result of converting idlKey to an @@ -824,29 +838,108 @@ function makeIterator (iterator, name, kind, keyIndex = 0, valueIndex = 1) { result = [key, value] break } + // 2. Return CreateIterResultObject(result, false). return { value: result, done: false } + } + } + + // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + // @ts-ignore + delete FastIterableIterator.prototype.constructor + + Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype) + + Object.defineProperties(FastIterableIterator.prototype, { + [Symbol.toStringTag]: { + writable: false, + enumerable: false, + configurable: true, + value: `${name} Iterator` }, - writable: true, - enumerable: true, - configurable: true + next: { writable: true, enumerable: true, configurable: true } }) - // The class string of an iterator prototype object for a given interface is the - // result of concatenating the identifier of the interface and the string " Iterator". - Object.defineProperty(iteratorObject, Symbol.toStringTag, { - value: `${name} Iterator`, - writable: false, - enumerable: false, - configurable: true - }) + /** + * @param {unknown} target + * @param {'key' | 'value' | 'key+value'} kind + * @returns {IterableIterator} + */ + return function (target, kind) { + return new FastIterableIterator(target, kind) + } +} - // esIteratorPrototype needs to be the prototype of iteratorObject - // which is the prototype of an empty object. Yes, it's confusing. - return Object.create(iteratorObject) +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {string} name name of the instance + * @param {any} object class + * @param {symbol} kInternalIterator + * @param {string | number} [keyIndex] + * @param {string | number} [valueIndex] + */ +function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) { + const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex) + + const properties = { + keys: { + writable: true, + enumerable: true, + configurable: true, + value: function keys () { + webidl.brandCheck(this, object) + return makeIterator(this, 'key') + } + }, + values: { + writable: true, + enumerable: true, + configurable: true, + value: function values () { + webidl.brandCheck(this, object) + return makeIterator(this, 'value') + } + }, + entries: { + writable: true, + enumerable: true, + configurable: true, + value: function entries () { + webidl.brandCheck(this, object) + return makeIterator(this, 'key+value') + } + }, + forEach: { + writable: true, + enumerable: true, + configurable: true, + value: function forEach (callbackfn, thisArg = globalThis) { + webidl.brandCheck(this, object) + webidl.argumentLengthCheck(arguments, 1, { header: `${name}.forEach` }) + if (typeof callbackfn !== 'function') { + throw new TypeError( + `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.` + ) + } + for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) { + callbackfn.call(thisArg, value, key, this) + } + } + } + } + + return Object.defineProperties(object.prototype, { + ...properties, + [Symbol.iterator]: { + writable: true, + enumerable: false, + configurable: true, + value: properties.entries.value + } + }) } /** @@ -1340,7 +1433,6 @@ module.exports = { isCancelled, createDeferredPromise, ReadableStreamFrom, - toUSVString, tryUpgradeRequestToAPotentiallyTrustworthyURL, clampAndCoarsenConnectionTimingInfo, coarsenedSharedCurrentTime, @@ -1365,7 +1457,8 @@ module.exports = { sameOrigin, normalizeMethod, serializeJavascriptValueToJSONString, - makeIterator, + iteratorMixin, + createIterator, isValidHeaderName, isValidHeaderValue, isErrorLike, diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index d639b8b7668..a93a25505fc 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -1,7 +1,7 @@ 'use strict' const { types } = require('node:util') -const { toUSVString } = require('./util') +const { toUSVString } = require('../core/util') /** @type {import('../../types/webidl').Webidl} */ const webidl = {} diff --git a/test/fetch/headers.js b/test/fetch/headers.js index b61d8b612d2..fcdf4b7a820 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -472,6 +472,21 @@ test('Headers as Iterable', async (t) => { deepStrictEqual([...headers], expected) }) + + await t.test('always use the same prototype Iterator', (t) => { + const HeadersIteratorNext = Function.call.bind(new Headers()[Symbol.iterator]().next) + + const init = [ + ['a', '1'], + ['b', '2'] + ] + + const headers = new Headers(init) + const iterator = headers[Symbol.iterator]() + assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[0], done: false }) + assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[1], done: false }) + assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: undefined, done: true }) + }) }) test('arg validation', () => { From 5f02182d5b962d8793f9a324bc0ab1036b354340 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 13 Feb 2024 10:31:15 +0100 Subject: [PATCH 027/123] chore: migrate a batch of tests to node test runner no. 9, remove tap (#2746) * chore: migrate a batch of tests to node test runner * add connect-timeout.js * remove tap --- .taprc | 7 - package.json | 7 +- test/client-request.js | 411 ++++++++------- test/client.js | 1033 ++++++++++++++++++++----------------- test/close-and-destroy.js | 181 ++++--- test/connect-timeout.js | 27 +- test/gc.js | 41 +- test/issue-2590.js | 10 +- test/pool.js | 470 +++++++++-------- test/request-timeout.js | 350 +++++++------ test/tls-session-reuse.js | 44 +- 11 files changed, 1439 insertions(+), 1142 deletions(-) delete mode 100644 .taprc diff --git a/.taprc b/.taprc deleted file mode 100644 index 61f70513a70..00000000000 --- a/.taprc +++ /dev/null @@ -1,7 +0,0 @@ -ts: false -jsx: false -flow: false -coverage: false -expose-gc: true -timeout: 60 -check-coverage: false diff --git a/package.json b/package.json index 92227f2799e..d03da7e116d 100644 --- a/package.json +++ b/package.json @@ -76,15 +76,15 @@ "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", - "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test", + "test": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test", "test:cookies": "borp --coverage -p \"test/cookie/*.js\"", "test:node-fetch": "borp --coverage -p \"test/node-fetch/**/*.js\"", "test:eventsource": "npm run build:node && borp --expose-gc --coverage -p \"test/eventsource/*.js\"", "test:fetch": "npm run build:node && borp --expose-gc --coverage -p \"test/fetch/*.js\" && borp --coverage -p \"test/webidl/*.js\"", "test:jest": "jest", - "test:tap": "tap test/*.js", + "test:unit": "borp --expose-gc -p \"test/*.js\"", "test:node-test": "borp --coverage -p \"test/node-test/**/*.js\"", - "test:tdd": "tap test/*.js --coverage -w", + "test:tdd": "borp --coverage --expose-gc -p \"test/*.js\"", "test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w", "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts", "test:websocket": "borp --coverage -p \"test/websocket/*.js\"", @@ -130,7 +130,6 @@ "snazzy": "^9.0.0", "standard": "^17.0.0", "superagent": "^8.1.2", - "tap": "^16.1.0", "tsd": "^0.30.1", "typescript": "^5.0.2", "wait-on": "^7.0.1", diff --git a/test/client-request.js b/test/client-request.js index cafcd39989b..2b73d3b01d0 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -2,7 +2,8 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') const EE = require('node:events') @@ -13,51 +14,53 @@ const { promisify } = require('node:util') const { NotSupportedError } = require('../lib/core/errors') const { parseFormDataString } = require('./utils/formdata') -test('request dump', (t) => { - t.plan(3) +test('request dump', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) let dumped = false client.on('disconnect', () => { - t.equal(dumped, true) + t.strictEqual(dumped, true) }) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.dump().then(() => { dumped = true t.ok(true, 'pass') }) }) }) + + await t.completed }) -test('request dump with abort signal', (t) => { - t.plan(2) +test('request dump with abort signal', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) let ac if (!global.AbortController) { const { AbortController } = require('abort-controller') @@ -66,50 +69,54 @@ test('request dump with abort signal', (t) => { ac = new AbortController() } body.dump({ signal: ac.signal }).catch((err) => { - t.equal(err.name, 'AbortError') + t.strictEqual(err.name, 'AbortError') server.close() }) ac.abort() }) }) + + await t.completed }) -test('request hwm', (t) => { - t.plan(2) +test('request hwm', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', highWaterMark: 1000 }, (err, { body }) => { - t.error(err) - t.same(body.readableHighWaterMark, 1000) + t.ifError(err) + t.deepStrictEqual(body.readableHighWaterMark, 1000) body.dump() }) }) + + await t.completed }) -test('request abort before headers', (t) => { - t.plan(6) +test('request abort before headers', async (t) => { + t = tspl(t, { plan: 6 }) const signal = new EE() const server = createServer((req, res) => { res.end('hello') signal.emit('abort') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client[kConnect](() => { client.request({ @@ -118,9 +125,9 @@ test('request abort before headers', (t) => { signal }, (err) => { t.ok(err instanceof errors.RequestAbortedError) - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) client.request({ path: '/', @@ -128,23 +135,25 @@ test('request abort before headers', (t) => { signal }, (err) => { t.ok(err instanceof errors.RequestAbortedError) - t.equal(signal.listenerCount('abort'), 0) + t.strictEqual(signal.listenerCount('abort'), 0) }) - t.equal(signal.listenerCount('abort'), 2) + t.strictEqual(signal.listenerCount('abort'), 2) }) }) + + await t.completed }) -test('request body destroyed on invalid callback', (t) => { - t.plan(1) +test('request body destroyed on invalid callback', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const body = new Readable({ read () {} @@ -156,24 +165,26 @@ test('request body destroyed on invalid callback', (t) => { body }, null) } catch (err) { - t.equal(body.destroyed, true) + t.strictEqual(body.destroyed, true) } }) + + await t.completed }) -test('trailers', (t) => { - t.plan(1) +test('trailers', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.writeHead(200, { Trailer: 'Content-MD5' }) res.addTrailers({ 'Content-MD5': 'test' }) res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { body, trailers } = await client.request({ path: '/', @@ -183,13 +194,15 @@ test('trailers', (t) => { body .on('data', () => t.fail()) .on('end', () => { - t.strictSame(trailers, { 'content-md5': 'test' }) + t.deepStrictEqual(trailers, { 'content-md5': 'test' }) }) }) + + await t.completed }) -test('destroy socket abruptly', { skip: true }, async (t) => { - t.plan(2) +test('destroy socket abruptly', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((socket) => { const lines = [ @@ -205,18 +218,18 @@ test('destroy socket abruptly', { skip: true }, async (t) => { // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { statusCode, body } = await client.request({ path: '/', method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) body.setEncoding('utf8') @@ -226,11 +239,11 @@ test('destroy socket abruptly', { skip: true }, async (t) => { actual += chunk } - t.equal(actual, 'the body') + t.strictEqual(actual, 'the body') }) -test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => { - t.plan(2) +test('destroy socket abruptly with keep-alive', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((socket) => { const lines = [ @@ -247,18 +260,18 @@ test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => { // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { statusCode, body } = await client.request({ path: '/', method: 'GET' }) - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) body.setEncoding('utf8') @@ -274,81 +287,87 @@ test('destroy socket abruptly with keep-alive', { skip: true }, async (t) => { } }) -test('request json', (t) => { - t.plan(1) +test('request json', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', method: 'GET' }) - t.strictSame(obj, await body.json()) + t.deepStrictEqual(obj, await body.json()) }) + + await t.completed }) -test('request long multibyte json', (t) => { - t.plan(1) +test('request long multibyte json', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: 'あ'.repeat(100000) } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', method: 'GET' }) - t.strictSame(obj, await body.json()) + t.deepStrictEqual(obj, await body.json()) }) + + await t.completed }) -test('request text', (t) => { - t.plan(1) +test('request text', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', method: 'GET' }) - t.strictSame(JSON.stringify(obj), await body.text()) + t.strictEqual(JSON.stringify(obj), await body.text()) }) + + await t.completed }) -test('empty host header', (t) => { - t.plan(3) +test('empty host header', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end(req.headers.host) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const serverAddress = `localhost:${server.address().port}` const client = new Client(`http://${serverAddress}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const getWithHost = async (host, wanted) => { const { body } = await client.request({ @@ -356,49 +375,53 @@ test('empty host header', (t) => { method: 'GET', headers: { host } }) - t.strictSame(await body.text(), wanted) + t.strictEqual(await body.text(), wanted) } await getWithHost('test', 'test') await getWithHost(undefined, serverAddress) await getWithHost('', '') }) + + await t.completed }) -test('request long multibyte text', (t) => { - t.plan(1) +test('request long multibyte text', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: 'あ'.repeat(100000) } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', method: 'GET' }) - t.strictSame(JSON.stringify(obj), await body.text()) + t.strictEqual(JSON.stringify(obj), await body.text()) }) + + await t.completed }) -test('request blob', (t) => { - t.plan(2) +test('request blob', async (t) => { + t = tspl(t, { plan: 2 }) const obj = { asd: true } const server = createServer((req, res) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -406,23 +429,25 @@ test('request blob', (t) => { }) const blob = await body.blob() - t.strictSame(obj, JSON.parse(await blob.text())) - t.equal(blob.type, 'application/json') + t.deepStrictEqual(obj, JSON.parse(await blob.text())) + t.strictEqual(blob.type, 'application/json') }) + + await t.completed }) -test('request arrayBuffer', (t) => { - t.plan(2) +test('request arrayBuffer', async (t) => { + t = tspl(t, { plan: 2 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -430,23 +455,25 @@ test('request arrayBuffer', (t) => { }) const ab = await body.arrayBuffer() - t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(ab)) + t.deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(ab)) t.ok(ab instanceof ArrayBuffer) }) + + await t.completed }) -test('request body', (t) => { - t.plan(1) +test('request body', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -457,26 +484,28 @@ test('request body', (t) => { for await (const chunk of body.body) { x += Buffer.from(chunk) } - t.strictSame(JSON.stringify(obj), x) + t.strictEqual(JSON.stringify(obj), x) }) + + await t.completed }) -test('request post body no missing data', (t) => { - t.plan(2) +test('request post body no missing data', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } - t.equal(ret, 'asd') + t.strictEqual(ret, 'asd') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -492,24 +521,26 @@ test('request post body no missing data', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body no extra data handler', (t) => { - t.plan(3) +test('request post body no extra data handler', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer(async (req, res) => { let ret = '' for await (const chunk of req) { ret += chunk } - t.equal(ret, 'asd') + t.strictEqual(ret, 'asd') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const reqBody = new Readable({ read () { @@ -518,7 +549,7 @@ test('request post body no extra data handler', (t) => { } }) process.nextTick(() => { - t.equal(reqBody.listenerCount('data'), 0) + t.strictEqual(reqBody.listenerCount('data'), 0) }) const { body } = await client.request({ path: '/', @@ -529,46 +560,50 @@ test('request post body no extra data handler', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request with onInfo callback', (t) => { - t.plan(3) +test('request with onInfo callback', async (t) => { + t = tspl(t, { plan: 3 }) const infos = [] const server = createServer((req, res) => { res.writeProcessing() res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ foo: 'bar' })) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) await client.request({ path: '/', method: 'GET', onInfo: (x) => { infos.push(x) } }) - t.equal(infos.length, 1) - t.equal(infos[0].statusCode, 102) + t.strictEqual(infos.length, 1) + t.strictEqual(infos[0].statusCode, 102) t.ok(true, 'pass') }) + + await t.completed }) -test('request with onInfo callback but socket is destroyed before end of response', (t) => { - t.plan(5) +test('request with onInfo callback but socket is destroyed before end of response', async (t) => { + t = tspl(t, { plan: 5 }) const infos = [] let response const server = createServer((req, res) => { response = res res.writeProcessing() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) try { await client.request({ path: '/', @@ -578,19 +613,21 @@ test('request with onInfo callback but socket is destroyed before end of respons response.destroy() } }) - t.error() + t.fail() } catch (e) { t.ok(e) - t.equal(e.message, 'other side closed') + t.strictEqual(e.message, 'other side closed') } - t.equal(infos.length, 1) - t.equal(infos[0].statusCode, 102) + t.strictEqual(infos.length, 1) + t.strictEqual(infos[0].statusCode, 102) t.ok(true, 'pass') }) + + await t.completed }) test('request onInfo callback headers parsing', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const infos = [] const server = net.createServer((socket) => { @@ -606,12 +643,12 @@ test('request onInfo callback headers parsing', async (t) => { ] socket.end(lines.join('\r\n')) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { body } = await client.request({ path: '/', @@ -619,14 +656,14 @@ test('request onInfo callback headers parsing', async (t) => { onInfo: (x) => { infos.push(x) } }) await body.dump() - t.equal(infos.length, 1) - t.equal(infos[0].statusCode, 103) - t.same(infos[0].headers, { link: '; rel=preload; as=style' }) + t.strictEqual(infos.length, 1) + t.strictEqual(infos[0].statusCode, 103) + t.deepStrictEqual(infos[0].headers, { link: '; rel=preload; as=style' }) t.ok(true, 'pass') }) test('request raw responseHeaders', async (t) => { - t.plan(4) + t = tspl(t, { plan: 4 }) const infos = [] const server = net.createServer((socket) => { @@ -642,12 +679,12 @@ test('request raw responseHeaders', async (t) => { ] socket.end(lines.join('\r\n')) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { body, headers } = await client.request({ path: '/', @@ -656,24 +693,24 @@ test('request raw responseHeaders', async (t) => { onInfo: (x) => { infos.push(x) } }) await body.dump() - t.equal(infos.length, 1) - t.same(infos[0].headers, ['Link', '; rel=preload; as=style']) - t.same(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) + t.strictEqual(infos.length, 1) + t.deepStrictEqual(infos[0].headers, ['Link', '; rel=preload; as=style']) + t.deepStrictEqual(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) t.ok(true, 'pass') }) -test('request formData', (t) => { - t.plan(1) +test('request formData', async (t) => { + t = tspl(t, { plan: 1 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -687,20 +724,22 @@ test('request formData', (t) => { t.ok(error instanceof NotSupportedError) } }) + + await t.completed }) -test('request text2', (t) => { - t.plan(2) +test('request text2', async (t) => { + t = tspl(t, { plan: 2 }) const obj = { asd: true } const server = createServer((req, res) => { res.end(JSON.stringify(obj)) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -711,13 +750,15 @@ test('request text2', (t) => { body.on('data', chunk => { ret += chunk }).on('end', () => { - t.equal(JSON.stringify(obj), ret) + t.strictEqual(JSON.stringify(obj), ret) }) - t.strictSame(JSON.stringify(obj), await p) + t.strictEqual(JSON.stringify(obj), await p) }) + + await t.completed }) -test('request with FormData body', (t) => { +test('request with FormData body', async (t) => { const { FormData } = require('../') const { Blob } = require('node:buffer') @@ -741,10 +782,10 @@ test('request with FormData body', (t) => { contentType ) - t.same(fields[0], { key: 'key', value: 'value' }) + t.deepStrictEqual(fields[0], { key: 'key', value: 'value' }) t.ok(fileMap.has('file')) - t.equal(fileMap.get('file').data.toString(), 'Hello, world!') - t.same(fileMap.get('file').info, { + t.strictEqual(fileMap.get('file').data.toString(), 'Hello, world!') + t.deepStrictEqual(fileMap.get('file').info, { filename: 'hello_world.txt', encoding: '7bit', mimeType: 'application/octet-stream' @@ -752,11 +793,11 @@ test('request with FormData body', (t) => { return res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) await client.request({ path: '/', @@ -766,10 +807,12 @@ test('request with FormData body', (t) => { t.end() }) + + await t.completed }) -test('request post body Buffer from string', (t) => { - t.plan(2) +test('request post body Buffer from string', async (t) => { + t = tspl(t, { plan: 2 }) const requestBody = Buffer.from('abcdefghijklmnopqrstuvwxyz') const server = createServer(async (req, res) => { @@ -777,14 +820,14 @@ test('request post body Buffer from string', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'abcdefghijklmnopqrstuvwxyz') + t.strictEqual(ret, 'abcdefghijklmnopqrstuvwxyz') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -795,10 +838,12 @@ test('request post body Buffer from string', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body Buffer from buffer', (t) => { - t.plan(2) +test('request post body Buffer from buffer', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = Buffer.from(fullBuffer.buffer, 8, 16) @@ -807,14 +852,14 @@ test('request post body Buffer from buffer', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -825,10 +870,12 @@ test('request post body Buffer from buffer', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body Uint8Array', (t) => { - t.plan(2) +test('request post body Uint8Array', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Uint8Array(fullBuffer.buffer, 8, 16) @@ -837,14 +884,14 @@ test('request post body Uint8Array', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -855,10 +902,12 @@ test('request post body Uint8Array', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body Uint32Array', (t) => { - t.plan(2) +test('request post body Uint32Array', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Uint32Array(fullBuffer.buffer, 8, 4) @@ -867,14 +916,14 @@ test('request post body Uint32Array', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -885,10 +934,12 @@ test('request post body Uint32Array', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body Float64Array', (t) => { - t.plan(2) +test('request post body Float64Array', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new Float64Array(fullBuffer.buffer, 8, 2) @@ -897,14 +948,14 @@ test('request post body Float64Array', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -915,10 +966,12 @@ test('request post body Float64Array', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body BigUint64Array', (t) => { - t.plan(2) +test('request post body BigUint64Array', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new BigUint64Array(fullBuffer.buffer, 8, 2) @@ -927,14 +980,14 @@ test('request post body BigUint64Array', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -945,10 +998,12 @@ test('request post body BigUint64Array', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) -test('request post body DataView', (t) => { - t.plan(2) +test('request post body DataView', async (t) => { + t = tspl(t, { plan: 2 }) const fullBuffer = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz') const requestBody = new DataView(fullBuffer.buffer, 8, 16) @@ -957,14 +1012,14 @@ test('request post body DataView', (t) => { for await (const chunk of req) { ret += chunk } - t.equal(ret, 'ijklmnopqrstuvwx') + t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { body } = await client.request({ path: '/', @@ -975,4 +1030,6 @@ test('request post body DataView', (t) => { await body.text() t.ok(true, 'pass') }) + + await t.completed }) diff --git a/test/client.js b/test/client.js index 7c3c8d0fcb3..155023f936e 100644 --- a/test/client.js +++ b/test/client.js @@ -1,9 +1,10 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') const { readFileSync, createReadStream } = require('node:fs') const { createServer } = require('node:http') const { Readable } = require('node:stream') -const { test } = require('tap') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { kSocket } = require('../lib/core/symbols') const { wrapWithAsyncIterable } = require('./utils/async-iterators') @@ -18,20 +19,20 @@ const hasIPv6 = (() => { ) })() -test('basic get', (t) => { - t.plan(24) +test('basic get', async (t) => { + t = tspl(t, { plan: 24 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(undefined, req.headers.foo) - t.equal('bar', req.headers.bar) - t.equal(undefined, req.headers['content-length']) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(undefined, req.headers.foo) + t.strictEqual('bar', req.headers.bar) + t.strictEqual(undefined, req.headers['content-length']) res.setHeader('Content-Type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const reqHeaders = { foo: undefined, @@ -42,9 +43,9 @@ test('basic get', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) - t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`) + t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`) const signal = new EE() client.request({ @@ -53,56 +54,58 @@ test('basic get', (t) => { method: 'GET', headers: reqHeaders }, (err, data) => { - t.error(err) + t.ifError(err) const { statusCode, headers, body } = data - t.equal(statusCode, 200) - t.equal(signal.listenerCount('abort'), 1) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(signal.listenerCount('abort'), 1) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal(signal.listenerCount('abort'), 0) - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual(signal.listenerCount('abort'), 0) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) client.request({ path: '/', method: 'GET', headers: reqHeaders }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic get with custom request.reset=true', (t) => { - t.plan(26) +test('basic get with custom request.reset=true', async (t) => { + t = tspl(t, { plan: 26 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) - t.equal(req.headers.connection, 'close') - t.equal(undefined, req.headers.foo) - t.equal('bar', req.headers.bar) - t.equal(undefined, req.headers['content-length']) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual(req.headers.connection, 'close') + t.strictEqual(undefined, req.headers.foo) + t.strictEqual('bar', req.headers.bar) + t.strictEqual(undefined, req.headers['content-length']) res.setHeader('Content-Type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const reqHeaders = { foo: undefined, @@ -111,9 +114,9 @@ test('basic get with custom request.reset=true', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, {}) - t.teardown(client.close.bind(client)) + after(() => client.close()) - t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`) + t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`) const signal = new EE() client.request({ @@ -123,21 +126,21 @@ test('basic get with custom request.reset=true', (t) => { reset: true, headers: reqHeaders }, (err, data) => { - t.error(err) + t.ifError(err) const { statusCode, headers, body } = data - t.equal(statusCode, 200) - t.equal(signal.listenerCount('abort'), 1) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(signal.listenerCount('abort'), 1) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal(signal.listenerCount('abort'), 0) - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual(signal.listenerCount('abort'), 0) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) client.request({ path: '/', @@ -145,26 +148,28 @@ test('basic get with custom request.reset=true', (t) => { method: 'GET', headers: reqHeaders }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic get with query params', (t) => { - t.plan(4) +test('basic get with query params', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) - t.strictSame(searchParamsObject, { + t.deepStrictEqual(searchParamsObject, { bool: 'true', foo: '1', bar: 'bar', @@ -177,7 +182,7 @@ test('basic get with query params', (t) => { res.statusCode = 200 res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const query = { bool: true, @@ -193,7 +198,7 @@ test('basic get with query params', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -202,21 +207,23 @@ test('basic get with query params', (t) => { method: 'GET', query }, (err, data) => { - t.error(err) + t.ifError(err) const { statusCode } = data - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('basic get with query params fails if url includes hashmark', (t) => { - t.plan(1) +test('basic get with query params fails if url includes hashmark', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const query = { foo: 1, @@ -228,7 +235,7 @@ test('basic get with query params fails if url includes hashmark', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -237,22 +244,24 @@ test('basic get with query params fails if url includes hashmark', (t) => { method: 'GET', query }, (err, data) => { - t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".') + t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".') }) }) + + await t.completed }) -test('basic get with empty query params', (t) => { - t.plan(4) +test('basic get with empty query params', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { const searchParamsObject = buildParams(req.url) - t.strictSame(searchParamsObject, {}) + t.deepStrictEqual(searchParamsObject, {}) res.statusCode = 200 res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const query = {} @@ -260,7 +269,7 @@ test('basic get with empty query params', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -269,21 +278,23 @@ test('basic get with empty query params', (t) => { method: 'GET', query }, (err, data) => { - t.error(err) + t.ifError(err) const { statusCode } = data - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('basic get with query params partially in path', (t) => { - t.plan(1) +test('basic get with query params partially in path', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { t.fail() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const query = { foo: 1 @@ -293,7 +304,7 @@ test('basic get with query params partially in path', (t) => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -302,25 +313,27 @@ test('basic get with query params partially in path', (t) => { method: 'GET', query }, (err, data) => { - t.equal(err.message, 'Query params cannot be passed when url already contains "?" or "#".') + t.strictEqual(err.message, 'Query params cannot be passed when url already contains "?" or "#".') }) }) + + await t.completed }) -test('basic get returns 400 when configured to throw on errors (callback)', (t) => { - t.plan(7) +test('basic get returns 400 when configured to throw on errors (callback)', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { res.statusCode = 400 res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -329,31 +342,33 @@ test('basic get returns 400 when configured to throw on errors (callback)', (t) method: 'GET', throwOnError: true }, (err) => { - t.equal(err.message, 'Response status code 400: Bad Request') - t.equal(err.status, 400) - t.equal(err.statusCode, 400) - t.equal(err.headers.connection, 'keep-alive') - t.equal(err.headers['content-length'], '5') - t.same(err.body, null) + t.strictEqual(err.message, 'Response status code 400: Bad Request') + t.strictEqual(err.status, 400) + t.strictEqual(err.statusCode, 400) + t.strictEqual(err.headers.connection, 'keep-alive') + t.strictEqual(err.headers['content-length'], '5') + t.strictEqual(err.body, undefined) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('basic get returns 400 when configured to throw on errors and correctly handles malformed json (callback)', (t) => { - t.plan(6) +test('basic get returns 400 when configured to throw on errors and correctly handles malformed json (callback)', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.writeHead(400, 'Invalid params', { 'content-type': 'application/json' }) res.end('Invalid params') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() client.request({ @@ -362,30 +377,32 @@ test('basic get returns 400 when configured to throw on errors and correctly han method: 'GET', throwOnError: true }, (err) => { - t.equal(err.message, 'Response status code 400: Invalid params') - t.equal(err.status, 400) - t.equal(err.statusCode, 400) - t.equal(err.headers.connection, 'keep-alive') - t.same(err.body, null) + t.strictEqual(err.message, 'Response status code 400: Invalid params') + t.strictEqual(err.status, 400) + t.strictEqual(err.statusCode, 400) + t.strictEqual(err.headers.connection, 'keep-alive') + t.strictEqual(err.body, undefined) }) - t.equal(signal.listenerCount('abort'), 1) + t.strictEqual(signal.listenerCount('abort'), 1) }) + + await t.completed }) -test('basic get returns 400 when configured to throw on errors (promise)', (t) => { - t.plan(6) +test('basic get returns 400 when configured to throw on errors (promise)', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.writeHead(400, 'Invalid params', { 'content-type': 'text/plain' }) res.end('Invalid params') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() try { @@ -397,18 +414,20 @@ test('basic get returns 400 when configured to throw on errors (promise)', (t) = }) t.fail('Should throw an error') } catch (err) { - t.equal(err.message, 'Response status code 400: Invalid params') - t.equal(err.status, 400) - t.equal(err.statusCode, 400) - t.equal(err.body, 'Invalid params') - t.equal(err.headers.connection, 'keep-alive') - t.equal(err.headers['content-type'], 'text/plain') + t.strictEqual(err.message, 'Response status code 400: Invalid params') + t.strictEqual(err.status, 400) + t.strictEqual(err.statusCode, 400) + t.strictEqual(err.body, 'Invalid params') + t.strictEqual(err.headers.connection, 'keep-alive') + t.strictEqual(err.headers['content-type'], 'text/plain') } }) + + await t.completed }) -test('basic get returns error body when configured to throw on errors', (t) => { - t.plan(6) +test('basic get returns error body when configured to throw on errors', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { const body = { msg: 'Error', details: { code: 94 } } @@ -418,13 +437,13 @@ test('basic get returns error body when configured to throw on errors', (t) => { }) res.end(bodyAsString) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const signal = new EE() try { @@ -436,36 +455,38 @@ test('basic get returns error body when configured to throw on errors', (t) => { }) t.fail('Should throw an error') } catch (err) { - t.equal(err.message, 'Response status code 400: Invalid params') - t.equal(err.status, 400) - t.equal(err.statusCode, 400) - t.equal(err.headers.connection, 'keep-alive') - t.equal(err.headers['content-type'], 'application/json') - t.same(err.body, { msg: 'Error', details: { code: 94 } }) + t.strictEqual(err.message, 'Response status code 400: Invalid params') + t.strictEqual(err.status, 400) + t.strictEqual(err.statusCode, 400) + t.strictEqual(err.headers.connection, 'keep-alive') + t.strictEqual(err.headers['content-type'], 'application/json') + t.deepStrictEqual(err.body, { msg: 'Error', details: { code: 94 } }) } }) + + await t.completed }) -test('basic head', (t) => { - t.plan(14) +test('basic head', async (t) => { + t = tspl(t, { plan: 14 }) const server = createServer((req, res) => { - t.equal('/123', req.url) - t.equal('HEAD', req.method) - t.equal(`localhost:${server.address().port}`, req.headers.host) + t.strictEqual('/123', req.url) + t.strictEqual('HEAD', req.method) + t.strictEqual(`localhost:${server.address().port}`, req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body .resume() .on('end', () => { @@ -474,9 +495,9 @@ test('basic head', (t) => { }) client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body .resume() .on('end', () => { @@ -484,28 +505,30 @@ test('basic head', (t) => { }) }) }) + + await t.completed }) -test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => { - t.plan(14) +test('basic head (IPv6)', { skip: !hasIPv6 }, async (t) => { + t = tspl(t, { plan: 15 }) const server = createServer((req, res) => { - t.equal('/123', req.url) - t.equal('HEAD', req.method) - t.equal(`[::1]:${server.address().port}`, req.headers.host) + t.strictEqual('/123', req.url) + t.strictEqual('HEAD', req.method) + t.strictEqual(`[::1]:${server.address().port}`, req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, '::', () => { const client = new Client(`http://[::1]:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body .resume() .on('end', () => { @@ -514,9 +537,9 @@ test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => { }) client.request({ path: '/123', method: 'HEAD' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body .resume() .on('end', () => { @@ -524,90 +547,96 @@ test('basic head (IPv6)', { skip: !hasIPv6 }, (t) => { }) }) }) + + await t.completed }) -test('get with host header', (t) => { - t.plan(7) +test('get with host header', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal('example.com', req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual('example.com', req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello from ' + req.headers.host) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello from example.com', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello from example.com', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('get with host header (IPv6)', { skip: !hasIPv6 }, (t) => { - t.plan(7) +test('get with host header (IPv6)', { skip: !hasIPv6 }, async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) - t.equal('[::1]', req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) + t.strictEqual('[::1]', req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello from ' + req.headers.host) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, '::', () => { const client = new Client(`http://[::1]:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: { host: '[::1]' } }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello from [::1]', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello from [::1]', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('head with host header', (t) => { - t.plan(7) +test('head with host header', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('HEAD', req.method) - t.equal('example.com', req.headers.host) + t.strictEqual('/', req.url) + t.strictEqual('HEAD', req.method) + t.strictEqual('example.com', req.headers.host) res.setHeader('content-type', 'text/plain') res.end('hello from ' + req.headers.host) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'HEAD', headers: { host: 'example.com' } }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body .resume() .on('end', () => { @@ -615,13 +644,15 @@ test('head with host header', (t) => { }) }) }) + + await t.completed }) function postServer (t, expected) { return function (req, res) { - t.equal(req.url, '/') - t.equal(req.method, 'POST') - t.notSame(req.headers['content-length'], null) + t.strictEqual(req.url, '/') + t.strictEqual(req.method, 'POST') + t.notStrictEqual(req.headers['content-length'], null) req.setEncoding('utf8') let data = '' @@ -629,74 +660,78 @@ function postServer (t, expected) { req.on('data', function (d) { data += d }) req.on('end', () => { - t.equal(data, expected) + t.strictEqual(data, expected) res.end('hello') }) } } -test('basic POST with string', (t) => { - t.plan(7) +test('basic POST with string', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'POST', body: expected }, (err, data) => { - t.error(err) - t.equal(data.statusCode, 200) + t.ifError(err) + t.strictEqual(data.statusCode, 200) const bufs = [] data.body .on('data', (buf) => { bufs.push(buf) }) .on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with empty string', (t) => { - t.plan(7) +test('basic POST with empty string', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer(postServer(t, '')) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'POST', body: '' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with string and content-length', (t) => { - t.plan(7) +test('basic POST with string and content-length', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -706,56 +741,60 @@ test('basic POST with string and content-length', (t) => { }, body: expected }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with Buffer', (t) => { - t.plan(7) +test('basic POST with Buffer', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename) const server = createServer(postServer(t, expected.toString())) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'POST', body: expected }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with stream', (t) => { - t.plan(7) +test('basic POST with stream', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -766,30 +805,32 @@ test('basic POST with stream', (t) => { headersTimeout: 0, body: createReadStream(__filename) }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with paused stream', (t) => { - t.plan(7) +test('basic POST with paused stream', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const stream = createReadStream(__filename) stream.pause() @@ -802,32 +843,34 @@ test('basic POST with paused stream', (t) => { headersTimeout: 0, body: stream }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with custom stream', (t) => { - t.plan(4) +test('basic POST with custom stream', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { req.resume().on('end', () => { res.end('hello') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = new EE() body.pipe = () => {} @@ -837,17 +880,17 @@ test('basic POST with custom stream', (t) => { headersTimeout: 0, body }, (err, data) => { - t.error(err) - t.equal(data.statusCode, 200) + t.ifError(err) + t.strictEqual(data.statusCode, 200) const bufs = [] data.body.on('data', (buf) => { bufs.push(buf) }) data.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) - t.strictSame(client[kBusy], true) + t.deepStrictEqual(client[kBusy], true) body.on('close', () => { body.emit('end') @@ -866,10 +909,12 @@ test('basic POST with custom stream', (t) => { }) }) }) + + await t.completed }) -test('basic POST with iterator', (t) => { - t.plan(3) +test('basic POST with iterator', async (t) => { + t = tspl(t, { plan: 3 }) const expected = 'hello' @@ -878,7 +923,7 @@ test('basic POST with iterator', (t) => { res.end(expected) }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const iterable = { [Symbol.iterator]: function * () { @@ -891,7 +936,7 @@ test('basic POST with iterator', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -899,24 +944,26 @@ test('basic POST with iterator', (t) => { requestTimeout: 0, body: iterable }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with iterator with invalid data', (t) => { - t.plan(1) +test('basic POST with iterator with invalid data', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer(() => {}) - t.teardown(server.close.bind(server)) + after(() => server.close()) const iterable = { [Symbol.iterator]: function * () { @@ -926,7 +973,7 @@ test('basic POST with iterator with invalid data', (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -937,19 +984,21 @@ test('basic POST with iterator with invalid data', (t) => { t.ok(err instanceof TypeError) }) }) + + await t.completed }) -test('basic POST with async iterator', (t) => { - t.plan(7) +test('basic POST with async iterator', async (t) => { + t = tspl(t, { plan: 7 }) const expected = readFileSync(__filename, 'utf8') const server = createServer(postServer(t, expected)) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', @@ -960,28 +1009,30 @@ test('basic POST with async iterator', (t) => { headersTimeout: 0, body: wrapWithAsyncIterable(createReadStream(__filename)) }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with transfer encoding: chunked', (t) => { - t.plan(8) +test('basic POST with transfer encoding: chunked', async (t) => { + t = tspl(t, { plan: 8 }) let body const server = createServer(function (req, res) { - t.equal(req.url, '/') - t.equal(req.method, 'POST') - t.same(req.headers['content-length'], null) - t.equal(req.headers['transfer-encoding'], 'chunked') + t.strictEqual(req.url, '/') + t.strictEqual(req.method, 'POST') + t.strictEqual(req.headers['content-length'], undefined) + t.strictEqual(req.headers['transfer-encoding'], 'chunked') body.push(null) @@ -991,15 +1042,15 @@ test('basic POST with transfer encoding: chunked', (t) => { req.on('data', function (d) { data += d }) req.on('end', () => { - t.equal(data, 'asd') + t.strictEqual(data, 'asd') res.end('hello') }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) body = new Readable({ read () { } @@ -1011,31 +1062,33 @@ test('basic POST with transfer encoding: chunked', (t) => { // no content-length header body }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('basic POST with empty stream', (t) => { - t.plan(4) +test('basic POST with empty stream', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer(function (req, res) { - t.same(req.headers['content-length'], 0) + t.deepStrictEqual(req.headers['content-length'], '0') req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = new Readable({ autoDestroy: false, @@ -1046,7 +1099,7 @@ test('basic POST with empty stream', (t) => { } }).on('end', () => { process.nextTick(() => { - t.equal(body.destroyed, true) + t.strictEqual(body.destroyed, true) }) }) body.push(null) @@ -1055,7 +1108,7 @@ test('basic POST with empty stream', (t) => { method: 'POST', body }, (err, { statusCode, headers, body }) => { - t.error(err) + t.ifError(err) body .on('data', () => { t.fail() @@ -1065,20 +1118,22 @@ test('basic POST with empty stream', (t) => { }) }) }) + + await t.completed }) -test('10 times GET', (t) => { +test('10 times GET', async (t) => { const num = 10 - t.plan(3 * 10) + t = tspl(t, { plan: 3 * num }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) for (let i = 0; i < num; i++) { makeRequest(i) @@ -1086,32 +1141,34 @@ test('10 times GET', (t) => { function makeRequest (i) { client.request({ path: '/' + i, method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('/' + i, Buffer.concat(bufs).toString('utf8')) + t.strictEqual('/' + i, Buffer.concat(bufs).toString('utf8')) }) }) } }) + + await t.completed }) -test('10 times HEAD', (t) => { +test('10 times HEAD', async (t) => { const num = 10 - t.plan(3 * 10) + t = tspl(t, { plan: num * 3 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) for (let i = 0; i < num; i++) { makeRequest(i) @@ -1119,8 +1176,8 @@ test('10 times HEAD', (t) => { function makeRequest (i) { client.request({ path: '/' + i, method: 'HEAD' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) body .resume() .on('end', () => { @@ -1129,49 +1186,53 @@ test('10 times HEAD', (t) => { }) } }) + + await t.completed }) -test('Set-Cookie', (t) => { - t.plan(4) +test('Set-Cookie', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.setHeader('content-type', 'text/plain') res.setHeader('Set-Cookie', ['a cookie', 'another cookie', 'more cookies']) res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.strictSame(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies']) + t.ifError(err) + t.strictEqual(statusCode, 200) + t.deepStrictEqual(headers['set-cookie'], ['a cookie', 'another cookie', 'more cookies']) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) }) + + await t.completed }) -test('ignore request header mutations', (t) => { - t.plan(2) +test('ignore request header mutations', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { - t.equal(req.headers.test, 'test') + t.strictEqual(req.headers.test, 'test') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const headers = { test: 'test' } client.request({ @@ -1179,20 +1240,22 @@ test('ignore request header mutations', (t) => { method: 'GET', headers }, (err, { body }) => { - t.error(err) + t.ifError(err) body.resume() }) headers.test = 'asd' }) + + await t.completed }) -test('url-like url', (t) => { - t.plan(1) +test('url-like url', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client({ @@ -1200,25 +1263,27 @@ test('url-like url', (t) => { port: server.address().port, protocol: 'http:' }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) }) + + await t.completed }) -test('an absolute url as path', (t) => { - t.plan(2) +test('an absolute url as path', async (t) => { + t = tspl(t, { plan: 2 }) const path = 'http://example.com' const server = createServer((req, res) => { - t.equal(req.url, path) + t.strictEqual(req.url, path) res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client({ @@ -1226,22 +1291,24 @@ test('an absolute url as path', (t) => { port: server.address().port, protocol: 'http:' }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path, method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) }) + + await t.completed }) -test('multiple destroy callback', (t) => { - t.plan(4) +test('multiple destroy callback', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client({ @@ -1249,51 +1316,53 @@ test('multiple destroy callback', (t) => { port: server.address().port, protocol: 'http:' }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() - .on('error', () => { - t.ok(true, 'pass') + .on('error', (err) => { + t.ok(err instanceof Error) }) client.destroy(new Error(), (err) => { - t.error(err) + t.ifError(err) }) client.destroy(new Error(), (err) => { - t.error(err) + t.ifError(err) }) }) }) + + await t.completed }) -test('only one streaming req at a time', (t) => { - t.plan(7) +test('only one streaming req at a time', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 4 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) @@ -1304,58 +1373,60 @@ test('only one streaming req at a time', (t) => { body: new Readable({ read () { setImmediate(() => { - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) this.push(null) }) } }).on('resume', () => { - t.equal(client[kSize], 1) + t.strictEqual(client[kSize], 1) }) }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { t.ok(true, 'pass') }) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) }) }) + + await t.completed }) -test('only one async iterating req at a time', (t) => { - t.plan(6) +test('only one async iterating req at a time', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 4 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) const body = wrapWithAsyncIterable(new Readable({ read () { setImmediate(() => { - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) this.push(null) }) } @@ -1366,48 +1437,52 @@ test('only one async iterating req at a time', (t) => { idempotent: true, body }, (err, data) => { - t.error(err) + t.ifError(err) data.body .resume() .on('end', () => { t.ok(true, 'pass') }) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) }) }) + + await t.completed }) -test('300 requests succeed', (t) => { - t.plan(300 * 3) +test('300 requests succeed', async (t) => { + t = tspl(t, { plan: 300 * 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) for (let n = 0; n < 300; ++n) { client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.on('data', (chunk) => { - t.equal(chunk.toString(), 'asd') + t.strictEqual(chunk.toString(), 'asd') }).on('end', () => { t.ok(true, 'pass') }) }) } }) + + await t.completed }) -test('request args validation', (t) => { - t.plan(2) +test('request args validation', async (t) => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:5000') @@ -1420,29 +1495,33 @@ test('request args validation', (t) => { } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) } + + await t.completed }) -test('request args validation promise', (t) => { - t.plan(1) +test('request args validation promise', async (t) => { + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:5000') client.request(null).catch((err) => { t.ok(err instanceof errors.InvalidArgumentError) }) + + await t.completed }) -test('increase pipelining', (t) => { - t.plan(4) +test('increase pipelining', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { req.resume() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', @@ -1462,34 +1541,36 @@ test('increase pipelining', (t) => { } }) - t.equal(client[kRunning], 0) + t.strictEqual(client[kRunning], 0) client.on('connect', () => { - t.equal(client[kRunning], 0) + t.strictEqual(client[kRunning], 0) process.nextTick(() => { - t.equal(client[kRunning], 1) + t.strictEqual(client[kRunning], 1) client.pipelining = 3 - t.equal(client[kRunning], 2) + t.strictEqual(client[kRunning], 2) }) }) }) + + await t.completed }) -test('destroy in push', (t) => { - t.plan(4) +test('destroy in push', async (t) => { + t = tspl(t, { plan: 4 }) let _res const server = createServer((req, res) => { res.write('asd') _res = res }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.once('data', () => { _res.write('asd') body.on('data', (buf) => { @@ -1502,53 +1583,57 @@ test('destroy in push', (t) => { }) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) let buf = '' body.on('data', (chunk) => { buf = chunk.toString() _res.end() }).on('end', () => { - t.equal('asd', buf) + t.strictEqual('asd', buf) }) }) }) + + await t.completed }) -test('non recoverable socket error fails pending request', (t) => { - t.plan(2) +test('non recoverable socket error fails pending request', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.equal(err.message, 'kaboom') + t.strictEqual(err.message, 'kaboom') }) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.equal(err.message, 'kaboom') + t.strictEqual(err.message, 'kaboom') }) client.on('connect', () => { client[kSocket].destroy(new Error('kaboom')) }) }) + + await t.completed }) -test('POST empty with error', (t) => { - t.plan(1) +test('POST empty with error', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = new Readable({ read () { @@ -1562,110 +1647,118 @@ test('POST empty with error', (t) => { }) client.request({ path: '/', method: 'POST', body }, (err, data) => { - t.equal(err.message, 'asd') + t.strictEqual(err.message, 'asd') }) }) + + await t.completed }) -test('busy', (t) => { - t.plan(2) +test('busy', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client[kConnect](() => { client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) - t.equal(client[kBusy], true) + t.strictEqual(client[kBusy], true) }) }) + + await t.completed }) -test('connected', (t) => { - t.plan(7) +test('connected', async (t) => { + t = tspl(t, { plan: 7 }) const server = createServer((req, res) => { // needed so that disconnect is emitted res.setHeader('connection', 'close') req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const url = new URL(`http://localhost:${server.address().port}`) const client = new Client(url, { pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.on('connect', (origin, [self]) => { - t.equal(origin, url) - t.equal(client, self) + t.strictEqual(origin, url) + t.strictEqual(client, self) }) client.on('disconnect', (origin, [self]) => { - t.equal(origin, url) - t.equal(client, self) + t.strictEqual(origin, url) + t.strictEqual(client, self) }) - t.equal(client[kConnected], false) + t.strictEqual(client[kConnected], false) client[kConnect](() => { client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) - t.equal(client[kConnected], true) + t.strictEqual(client[kConnected], true) }) }) + + await t.completed }) -test('emit disconnect after destroy', t => { - t.plan(4) +test('emit disconnect after destroy', async t => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { req.pipe(res) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const url = new URL(`http://localhost:${server.address().port}`) const client = new Client(url) - t.equal(client[kConnected], false) + t.strictEqual(client[kConnected], false) client[kConnect](() => { - t.equal(client[kConnected], true) + t.strictEqual(client[kConnected], true) let disconnected = false client.on('disconnect', () => { disconnected = true t.ok(true, 'pass') }) client.destroy(() => { - t.equal(disconnected, true) + t.strictEqual(disconnected, true) }) }) }) + + await t.completed }) -test('end response before request', t => { - t.plan(2) +test('end response before request', async t => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1688,13 +1781,15 @@ test('end response before request', t => { }) .resume() client.on('disconnect', (url, targets, err) => { - t.equal(err.code, 'UND_ERR_INFO') + t.strictEqual(err.code, 'UND_ERR_INFO') }) }) + + await t.completed }) -test('parser pause with no body timeout', (t) => { - t.plan(2) +test('parser pause with no body timeout', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { let counter = 0 const t = setInterval(() => { @@ -1708,67 +1803,71 @@ test('parser pause with no body timeout', (t) => { } }, 20) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) body.resume() }) }) + + await t.completed }) -test('TypedArray and DataView body', (t) => { - t.plan(3) +test('TypedArray and DataView body', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { - t.equal(req.headers['content-length'], '8') + t.strictEqual(req.headers['content-length'], '8') res.end() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = Uint8Array.from(Buffer.alloc(8)) client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) body.resume() }) }) + + await t.completed }) -test('async iterator empty chunk continues', (t) => { - t.plan(5) +test('async iterator empty chunk continues', async (t) => { + t = tspl(t, { plan: 5 }) const serverChunks = ['hello', 'world'] const server = createServer((req, res) => { let str = '' let i = 0 req.on('data', (chunk) => { const content = chunk.toString() - t.equal(serverChunks[i++], content) + t.strictEqual(serverChunks[i++], content) str += content }).on('end', () => { - t.equal(str, serverChunks.join('')) + t.strictEqual(str, serverChunks.join('')) res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = (async function * () { yield serverChunks[0] @@ -1776,27 +1875,29 @@ test('async iterator empty chunk continues', (t) => { yield serverChunks[1] })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) body.resume() }) }) + + await t.completed }) -test('async iterator error from server destroys early', (t) => { - t.plan(3) +test('async iterator error from server destroys early', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.on('data', (chunk) => { res.destroy() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) let gotDestroyed const body = (async function * () { try { @@ -1813,14 +1914,16 @@ test('async iterator error from server destroys early', (t) => { })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { t.ok(err) - t.equal(statusCode, undefined) + t.strictEqual(statusCode, undefined) gotDestroyed() }) }) + + await t.completed }) -test('regular iterator error from server closes early', (t) => { - t.plan(3) +test('regular iterator error from server closes early', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.on('data', () => { process.nextTick(() => { @@ -1828,13 +1931,13 @@ test('regular iterator error from server closes early', (t) => { }) }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) let gotDestroyed = false const body = (function * () { try { @@ -1853,27 +1956,28 @@ test('regular iterator error from server closes early', (t) => { })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { t.ok(err) - t.equal(statusCode, undefined) + t.strictEqual(statusCode, undefined) gotDestroyed = true }) }) + await t.completed }) -test('async iterator early return closes early', (t) => { - t.plan(3) +test('async iterator early return closes early', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.on('data', () => { res.writeHead(200) res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) let gotDestroyed const body = (async function * () { try { @@ -1889,28 +1993,29 @@ test('async iterator early return closes early', (t) => { } })() client.request({ path: '/', method: 'POST', body }, (err, { statusCode, body }) => { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) gotDestroyed() }) }) + await t.completed }) -test('async iterator yield unsupported TypedArray', (t) => { - t.plan(3) +test('async iterator yield unsupported TypedArray', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.on('end', () => { res.writeHead(200) res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = (async function * () { try { yield new Int32Array([1]) @@ -1921,26 +2026,28 @@ test('async iterator yield unsupported TypedArray', (t) => { })() client.request({ path: '/', method: 'POST', body }, (err) => { t.ok(err) - t.equal(err.code, 'ERR_INVALID_ARG_TYPE') + t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE') }) }) + + await t.completed }) -test('async iterator yield object error', (t) => { - t.plan(3) +test('async iterator yield object error', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { req.on('end', () => { res.writeHead(200) res.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 0 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const body = (async function * () { try { yield {} @@ -1951,9 +2058,11 @@ test('async iterator yield object error', (t) => { })() client.request({ path: '/', method: 'POST', body }, (err) => { t.ok(err) - t.equal(err.code, 'ERR_INVALID_ARG_TYPE') + t.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE') }) }) + + await t.completed }) function buildParams (path) { @@ -1979,8 +2088,8 @@ function buildParams (path) { return builtParams } -test('\\r\\n in Headers', (t) => { - t.plan(1) +test('\\r\\n in Headers', async (t) => { + t = tspl(t, { plan: 1 }) const reqHeaders = { bar: '\r\nbar' @@ -1989,19 +2098,19 @@ test('\\r\\n in Headers', (t) => { const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: reqHeaders }, (err) => { - t.equal(err.message, 'invalid bar header') + t.strictEqual(err.message, 'invalid bar header') }) }) -test('\\r in Headers', (t) => { - t.plan(1) +test('\\r in Headers', async (t) => { + t = tspl(t, { plan: 1 }) const reqHeaders = { bar: '\rbar' @@ -2010,19 +2119,19 @@ test('\\r in Headers', (t) => { const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: reqHeaders }, (err) => { - t.equal(err.message, 'invalid bar header') + t.strictEqual(err.message, 'invalid bar header') }) }) -test('\\n in Headers', (t) => { - t.plan(1) +test('\\n in Headers', async (t) => { + t = tspl(t, { plan: 1 }) const reqHeaders = { bar: '\nbar' @@ -2031,19 +2140,19 @@ test('\\n in Headers', (t) => { const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: reqHeaders }, (err) => { - t.equal(err.message, 'invalid bar header') + t.strictEqual(err.message, 'invalid bar header') }) }) -test('\\n in Headers', (t) => { - t.plan(1) +test('\\n in Headers', async (t) => { + t = tspl(t, { plan: 1 }) const reqHeaders = { '\nbar': 'foo' @@ -2052,45 +2161,45 @@ test('\\n in Headers', (t) => { const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET', headers: reqHeaders }, (err) => { - t.equal(err.message, 'invalid header key') + t.strictEqual(err.message, 'invalid header key') }) }) -test('\\n in Path', (t) => { - t.plan(1) +test('\\n in Path', async (t) => { + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/\n', method: 'GET' }, (err) => { - t.equal(err.message, 'invalid request path') + t.strictEqual(err.message, 'invalid request path') }) }) -test('\\n in Method', (t) => { - t.plan(1) +test('\\n in Method', async (t) => { + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:4242', { keepAliveTimeout: 300e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.request({ path: '/', method: 'GET\n' }, (err) => { - t.equal(err.message, 'invalid request method') + t.strictEqual(err.message, 'invalid request method') }) }) diff --git a/test/close-and-destroy.js b/test/close-and-destroy.js index b946e3e18b6..5d4f135dcd2 100644 --- a/test/close-and-destroy.js +++ b/test/close-and-destroy.js @@ -1,12 +1,13 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { Client, errors } = require('..') const { createServer } = require('node:http') const { kSocket, kSize } = require('../lib/core/symbols') -test('close waits for queued requests to finish', (t) => { - t.plan(16) +test('close waits for queued requests to finish', async (t) => { + t = tspl(t, { plan: 16 }) const server = createServer() @@ -14,11 +15,11 @@ test('close waits for queued requests to finish', (t) => { t.ok(true, 'request received') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, function (err, data) { onRequest(err, data) @@ -36,36 +37,38 @@ test('close waits for queued requests to finish', (t) => { }) function onRequest (err, { statusCode, headers, body }) { - t.error(err) - t.equal(statusCode, 200) + t.ifError(err) + t.strictEqual(statusCode, 200) const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) } + + await t.completed }) -test('destroy invoked all pending callbacks', (t) => { - t.plan(4) +test('destroy invoked all pending callbacks', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer() server.on('request', (req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.on('error', (err) => { t.ok(err) }).resume() @@ -78,49 +81,53 @@ test('destroy invoked all pending callbacks', (t) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) + + await t.completed }) -test('destroy invoked all pending callbacks ticked', (t) => { - t.plan(4) +test('destroy invoked all pending callbacks ticked', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer() server.on('request', (req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) let ticked = false client.request({ path: '/', method: 'GET' }, (err) => { - t.equal(ticked, true) + t.strictEqual(ticked, true) t.ok(err instanceof errors.ClientDestroyedError) }) client.request({ path: '/', method: 'GET' }, (err) => { - t.equal(ticked, true) + t.strictEqual(ticked, true) t.ok(err instanceof errors.ClientDestroyedError) }) client.destroy() ticked = true }) + + await t.completed }) -test('close waits until socket is destroyed', (t) => { - t.plan(4) +test('close waits until socket is destroyed', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) makeRequest() @@ -130,39 +137,41 @@ test('close waits until socket is destroyed', (t) => { done = true }) client.close((err) => { - t.error(err) - t.equal(client.closed, true) - t.equal(done, true) + t.ifError(err) + t.strictEqual(client.closed, true) + t.strictEqual(done, true) }) }) function makeRequest () { client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) }) return client[kSize] <= client.pipelining } }) + + await t.completed }) -test('close should still reconnect', (t) => { - t.plan(6) +test('close should still reconnect', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) t.ok(makeRequest()) t.ok(!makeRequest()) client.close((err) => { - t.error(err) - t.equal(client.closed, true) + t.ifError(err) + t.strictEqual(client.closed, true) }) client.once('connect', () => { client[kSocket].destroy() @@ -170,57 +179,61 @@ test('close should still reconnect', (t) => { function makeRequest () { client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) return client[kSize] <= client.pipelining } }) + + await t.completed }) -test('close should call callback once finished', (t) => { - t.plan(6) +test('close should call callback once finished', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { setImmediate(function () { res.end(req.url) }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) t.ok(makeRequest()) t.ok(!makeRequest()) client.close((err) => { - t.error(err) - t.equal(client.closed, true) + t.ifError(err) + t.strictEqual(client.closed, true) }) function makeRequest () { client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.resume() }) return client[kSize] <= client.pipelining } }) + + await t.completed }) -test('closed and destroyed errors', (t) => { - t.plan(4) +test('closed and destroyed errors', async (t) => { + t = tspl(t, { plan: 4 }) const client = new Client('http://localhost:4000') - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err) => { t.ok(err) }) client.close((err) => { - t.error(err) + t.ifError(err) }) client.request({ path: '/', method: 'GET' }, (err) => { t.ok(err instanceof errors.ClientClosedError) @@ -229,13 +242,15 @@ test('closed and destroyed errors', (t) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) + + await t.completed }) -test('close after and destroy should error', (t) => { - t.plan(2) +test('close after and destroy should error', async (t) => { + t = tspl(t, { plan: 2 }) const client = new Client('http://localhost:4000') - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.destroy() client.close((err) => { @@ -244,16 +259,18 @@ test('close after and destroy should error', (t) => { client.close().catch((err) => { t.ok(err instanceof errors.ClientDestroyedError) }) + + await t.completed }) -test('close socket and reconnect after maxRequestsPerClient reached', (t) => { - t.plan(5) +test('close socket and reconnect after maxRequestsPerClient reached', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { let connections = 0 @@ -264,28 +281,30 @@ test('close socket and reconnect after maxRequestsPerClient reached', (t) => { `http://localhost:${server.address().port}`, { maxRequestsPerClient: 2 } ) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - t.equal(connections, 2) + await makeRequest() + await makeRequest() + await makeRequest() + await makeRequest() + t.strictEqual(connections, 2) function makeRequest () { return client.request({ path: '/', method: 'GET' }) } }) + + await t.completed }) -test('close socket and reconnect after maxRequestsPerClient reached (async)', (t) => { - t.plan(2) +test('close socket and reconnect after maxRequestsPerClient reached (async)', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { let connections = 0 @@ -296,32 +315,32 @@ test('close socket and reconnect after maxRequestsPerClient reached (async)', (t `http://localhost:${server.address().port}`, { maxRequestsPerClient: 2 } ) - t.teardown(client.destroy.bind(client)) - - await t.resolves( - Promise.all([ - makeRequest(), - makeRequest(), - makeRequest(), - makeRequest() - ]) - ) - t.equal(connections, 2) + after(() => client.destroy()) + + await Promise.all([ + makeRequest(), + makeRequest(), + makeRequest(), + makeRequest() + ]) + t.strictEqual(connections, 2) function makeRequest () { return client.request({ path: '/', method: 'GET' }) } }) + + await t.completed }) -test('should not close socket when no maxRequestsPerClient is provided', (t) => { - t.plan(5) +test('should not close socket when no maxRequestsPerClient is provided', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end(req.url) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { let connections = 0 @@ -329,16 +348,18 @@ test('should not close socket when no maxRequestsPerClient is provided', (t) => connections++ }) const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - await t.resolves(makeRequest()) - t.equal(connections, 1) + await makeRequest() + await makeRequest() + await makeRequest() + await makeRequest() + t.strictEqual(connections, 1) function makeRequest () { return client.request({ path: '/', method: 'GET' }) } }) + + await t.completed }) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index 5eab8ee1098..d8ff177504c 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -1,19 +1,22 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { Client, Pool, errors } = require('..') const net = require('node:net') +const assert = require('node:assert') const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(ms)) -test('prioritize socket errors over timeouts', (t) => { - t.plan(1) +// Using describe instead of test to avoid the timeout +describe('prioritize socket errors over timeouts', () => { + const t = tspl({ ...assert, after: () => {} }, { plan: 1 }) const connectTimeout = 1000 const client = new Pool('http://foobar.bar:1234', { connectTimeout: 2 }) client.request({ method: 'GET', path: '/foobar' }) .then(() => t.fail()) .catch((err) => { - t.equal(err.code, 'ENOTFOUND') + t.strictEqual(err.code, 'ENOTFOUND') }) // block for 1s which is enough for the dns lookup to complete and TO to fire @@ -25,13 +28,13 @@ net.connect = function (options) { return new net.Socket(options) } -test('connect-timeout', t => { - t.plan(1) +test('connect-timeout', async t => { + t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:9000', { connectTimeout: 1e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const timeout = setTimeout(() => { t.fail() @@ -44,15 +47,17 @@ test('connect-timeout', t => { t.ok(err instanceof errors.ConnectTimeoutError) clearTimeout(timeout) }) + + await t.completed }) -test('connect-timeout', t => { - t.plan(1) +test('connect-timeout', async t => { + t = tspl(t, { plan: 1 }) const client = new Pool('http://localhost:9000', { connectTimeout: 1e3 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) const timeout = setTimeout(() => { t.fail() @@ -65,4 +70,6 @@ test('connect-timeout', t => { t.ok(err instanceof errors.ConnectTimeoutError) clearTimeout(timeout) }) + + await t.completed }) diff --git a/test/gc.js b/test/gc.js index ab3b0e83dbf..91ea6ab170c 100644 --- a/test/gc.js +++ b/test/gc.js @@ -1,18 +1,23 @@ 'use strict' /* global WeakRef, FinalizationRegistry */ -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createServer } = require('node:net') const { Client, Pool } = require('..') -const SKIP = typeof WeakRef === 'undefined' || typeof FinalizationRegistry === 'undefined' +const SKIP = ( + typeof WeakRef === 'undefined' || + typeof FinalizationRegistry === 'undefined' || + typeof global.gc === 'undefined' +) setInterval(() => { global.gc() }, 100).unref() -test('gc should collect the client if, and only if, there are no active sockets', { skip: SKIP }, t => { - t.plan(4) +test('gc should collect the client if, and only if, there are no active sockets', { skip: SKIP }, async t => { + t = tspl(t, { plan: 4 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -21,15 +26,15 @@ test('gc should collect the client if, and only if, there are no active sockets' socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) let weakRef let disconnected = false const registry = new FinalizationRegistry((data) => { - t.equal(data, 'test') - t.equal(disconnected, true) - t.equal(weakRef.deref(), undefined) + t.strictEqual(data, 'test') + t.strictEqual(disconnected, true) + t.strictEqual(weakRef.deref(), undefined) }) server.listen(0, () => { @@ -47,14 +52,16 @@ test('gc should collect the client if, and only if, there are no active sockets' path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.resume() }) }) + + await t.completed }) -test('gc should collect the pool if, and only if, there are no active sockets', { skip: SKIP }, t => { - t.plan(4) +test('gc should collect the pool if, and only if, there are no active sockets', { skip: SKIP }, async t => { + t = tspl(t, { plan: 4 }) const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') @@ -63,15 +70,15 @@ test('gc should collect the pool if, and only if, there are no active sockets', socket.write('Connection: keep-alive\r\n') socket.write('\r\n\r\n') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) let weakRef let disconnected = false const registry = new FinalizationRegistry((data) => { - t.equal(data, 'test') - t.equal(disconnected, true) - t.equal(weakRef.deref(), undefined) + t.strictEqual(data, 'test') + t.strictEqual(disconnected, true) + t.strictEqual(weakRef.deref(), undefined) }) server.listen(0, () => { @@ -91,8 +98,10 @@ test('gc should collect the pool if, and only if, there are no active sockets', path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.resume() }) }) + + await t.completed }) diff --git a/test/issue-2590.js b/test/issue-2590.js index cd4913c642f..c5499bf4513 100644 --- a/test/issue-2590.js +++ b/test/issue-2590.js @@ -1,14 +1,16 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { request } = require('..') const { createServer } = require('node:http') const { once } = require('node:events') test('aborting request with custom reason', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer(() => {}).listen(0) - t.teardown(server.close.bind(server)) + after(() => server.close()) await once(server, 'listening') const timeout = AbortSignal.timeout(0) @@ -25,11 +27,13 @@ test('aborting request with custom reason', async (t) => { await t.rejects( request(`http://localhost:${server.address().port}`, { signal: ac.signal }), - ac.signal.reason + /Request aborted/ ) await t.rejects( request(`http://localhost:${server.address().port}`, { signal: ac2.signal }), { code: 'UND_ERR_ABORTED' } ) + + await t.completed }) diff --git a/test/pool.js b/test/pool.js index af6a019c7a0..4a22aedc84e 100644 --- a/test/pool.js +++ b/test/pool.js @@ -1,5 +1,7 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { EventEmitter } = require('node:events') const { createServer } = require('node:http') const net = require('node:net') @@ -10,7 +12,6 @@ const { } = require('node:stream') const { promisify } = require('node:util') const proxyquire = require('proxyquire') -const { test } = require('tap') const { kBusy, kPending, @@ -24,60 +25,60 @@ const { errors } = require('..') -test('throws when connection is infinite', (t) => { - t.plan(2) +test('throws when connection is infinite', async (t) => { + t = tspl(t, { plan: 2 }) try { new Pool(null, { connections: 0 / 0 }) // eslint-disable-line } catch (e) { t.ok(e instanceof errors.InvalidArgumentError) - t.equal(e.message, 'invalid connections') + t.strictEqual(e.message, 'invalid connections') } }) -test('throws when connections is negative', (t) => { - t.plan(2) +test('throws when connections is negative', async (t) => { + t = tspl(t, { plan: 2 }) try { new Pool(null, { connections: -1 }) // eslint-disable-line no-new } catch (e) { t.ok(e instanceof errors.InvalidArgumentError) - t.equal(e.message, 'invalid connections') + t.strictEqual(e.message, 'invalid connections') } }) -test('throws when connection is not number', (t) => { - t.plan(2) +test('throws when connection is not number', async (t) => { + t = tspl(t, { plan: 2 }) try { new Pool(null, { connections: true }) // eslint-disable-line no-new } catch (e) { t.ok(e instanceof errors.InvalidArgumentError) - t.equal(e.message, 'invalid connections') + t.strictEqual(e.message, 'invalid connections') } }) -test('throws when factory is not a function', (t) => { - t.plan(2) +test('throws when factory is not a function', async (t) => { + t = tspl(t, { plan: 2 }) try { new Pool(null, { factory: '' }) // eslint-disable-line no-new } catch (e) { t.ok(e instanceof errors.InvalidArgumentError) - t.equal(e.message, 'factory must be a function.') + t.strictEqual(e.message, 'factory must be a function.') } }) -test('does not throw when connect is a function', (t) => { - t.plan(1) +test('does not throw when connect is a function', async (t) => { + t = tspl(t, { plan: 1 }) t.doesNotThrow(() => new Pool('http://localhost', { connect: () => {} })) }) -test('connect/disconnect event(s)', (t) => { +test('connect/disconnect event(s)', async (t) => { const clients = 2 - t.plan(clients * 6) + t = tspl(t, { plan: clients * 6 }) const server = createServer((req, res) => { res.writeHead(200, { @@ -86,23 +87,23 @@ test('connect/disconnect event(s)', (t) => { }) res.end('ok') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const pool = new Pool(`http://localhost:${server.address().port}`, { connections: clients, keepAliveTimeoutThreshold: 100 }) - t.teardown(pool.close.bind(pool)) + after(() => pool.close()) pool.on('connect', (origin, [pool, client]) => { - t.equal(client instanceof Client, true) + t.strictEqual(client instanceof Client, true) }) pool.on('disconnect', (origin, [pool, client], error) => { t.ok(client instanceof Client) t.ok(error instanceof errors.InformationalError) - t.equal(error.code, 'UND_ERR_INFO') - t.equal(error.message, 'socket idle timeout') + t.strictEqual(error.code, 'UND_ERR_INFO') + t.strictEqual(error.message, 'socket idle timeout') }) for (let i = 0; i < clients; i++) { @@ -110,112 +111,118 @@ test('connect/disconnect event(s)', (t) => { path: '/', method: 'GET' }, (err, { headers, body }) => { - t.error(err) + t.ifError(err) body.resume() }) } }) + + await t.completed }) -test('basic get', (t) => { - t.plan(14) +test('basic get', async (t) => { + t = tspl(t, { plan: 14 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) - t.equal(client[kUrl].origin, `http://localhost:${server.address().port}`) + t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) - t.equal(client.destroyed, false) - t.equal(client.closed, false) + t.strictEqual(client.destroyed, false) + t.strictEqual(client.closed, false) client.close((err) => { - t.error(err) - t.equal(client.destroyed, true) + t.ifError(err) + t.strictEqual(client.destroyed, true) client.destroy((err) => { - t.error(err) + t.ifError(err) client.close((err) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) }) - t.equal(client.closed, true) + t.strictEqual(client.closed, true) }) + + await t.completed }) -test('URL as arg', (t) => { - t.plan(9) +test('URL as arg', async (t) => { + t = tspl(t, { plan: 9 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const url = new URL('http://localhost') url.port = server.address().port const client = new Pool(url) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) client.close((err) => { - t.error(err) + t.ifError(err) client.destroy((err) => { - t.error(err) + t.ifError(err) client.close((err) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) }) }) + + await t.completed }) -test('basic get error async/await', (t) => { - t.plan(2) +test('basic get error async/await', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) await client.request({ path: '/', method: 'GET' }) .catch((err) => { @@ -228,24 +235,28 @@ test('basic get error async/await', (t) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) + + await t.completed }) test('basic get with async/await', async (t) => { + t = tspl(t, { plan: 4 }) + const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' }) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') body.resume() await promisify(finished)(body) @@ -255,36 +266,40 @@ test('basic get with async/await', async (t) => { }) test('stream get async/await', async (t) => { + t = tspl(t, { plan: 4 }) + const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) await promisify(server.listen.bind(server))(0) const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) await client.stream({ path: '/', method: 'GET' }, ({ statusCode, headers }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') return new PassThrough() }) + + await t.completed }) -test('stream get error async/await', (t) => { - t.plan(1) +test('stream get error async/await', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) await client.stream({ path: '/', method: 'GET' }, () => { @@ -293,27 +308,29 @@ test('stream get error async/await', (t) => { t.ok(err) }) }) + + await t.completed }) -test('pipeline get', (t) => { - t.plan(5) +test('pipeline get', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const bufs = [] client.pipeline({ path: '/', method: 'GET' }, ({ statusCode, headers, body }) => { - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') return body }) .end() @@ -321,12 +338,16 @@ test('pipeline get', (t) => { bufs.push(buf) }) .on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) + + await t.completed }) -test('backpressure algorithm', (t) => { +test('backpressure algorithm', async (t) => { + t = tspl(t, { plan: 12 }) + const seen = [] let total = 0 @@ -361,11 +382,11 @@ test('backpressure algorithm', (t) => { pool.dispatch({}, noopHandler) const d1 = seen.shift() // d1 = c0 - t.equal(d1.id, 0) + t.strictEqual(d1.id, 0) const d2 = seen.shift() // d2 = c0 - t.equal(d2.id, 0) + t.strictEqual(d2.id, 0) - t.equal(d1.id, d2.id) + t.strictEqual(d1.id, d2.id) writeMore = false @@ -374,12 +395,12 @@ test('backpressure algorithm', (t) => { pool.dispatch({}, noopHandler) // d4 = c1 const d3 = seen.shift() - t.equal(d3.id, 0) + t.strictEqual(d3.id, 0) const d4 = seen.shift() - t.equal(d4.id, 1) + t.strictEqual(d4.id, 1) - t.equal(d3.id, d2.id) - t.not(d3.id, d4.id) + t.strictEqual(d3.id, d2.id) + t.notEqual(d3.id, d4.id) writeMore = true @@ -392,28 +413,28 @@ test('backpressure algorithm', (t) => { pool.dispatch({}, noopHandler) // d6 = c0 const d5 = seen.shift() - t.equal(d5.id, 1) + t.strictEqual(d5.id, 1) const d6 = seen.shift() - t.equal(d6.id, 0) + t.strictEqual(d6.id, 0) - t.equal(d5.id, d4.id) - t.equal(d3.id, d6.id) + t.strictEqual(d5.id, d4.id) + t.strictEqual(d3.id, d6.id) - t.equal(total, 3) + t.strictEqual(total, 3) t.end() }) -test('busy', (t) => { - t.plan(8 * 16 + 2 + 1) +test('busy', async (t) => { + t = tspl(t, { plan: 8 * 16 + 2 + 1 }) const server = createServer((req, res) => { - t.equal('/', req.url) - t.equal('GET', req.method) + t.strictEqual('/', req.url) + t.strictEqual('GET', req.method) res.setHeader('content-type', 'text/plain') res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const connections = 2 @@ -428,45 +449,47 @@ test('busy', (t) => { client.on('connect', () => { t.ok(true, 'pass') }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) for (let n = 1; n <= 8; ++n) { client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => { - t.error(err) - t.equal(statusCode, 200) - t.equal(headers['content-type'], 'text/plain') + t.ifError(err) + t.strictEqual(statusCode, 200) + t.strictEqual(headers['content-type'], 'text/plain') const bufs = [] body.on('data', (buf) => { bufs.push(buf) }) body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) - t.equal(client[kPending], n) - t.equal(client[kBusy], n > 1) - t.equal(client[kSize], n) - t.equal(client[kRunning], 0) - - t.equal(client.stats.connected, 0) - t.equal(client.stats.free, 0) - t.equal(client.stats.queued, Math.max(n - connections, 0)) - t.equal(client.stats.pending, n) - t.equal(client.stats.size, n) - t.equal(client.stats.running, 0) + t.strictEqual(client[kPending], n) + t.strictEqual(client[kBusy], n > 1) + t.strictEqual(client[kSize], n) + t.strictEqual(client[kRunning], 0) + + t.strictEqual(client.stats.connected, 0) + t.strictEqual(client.stats.free, 0) + t.strictEqual(client.stats.queued, Math.max(n - connections, 0)) + t.strictEqual(client.stats.pending, n) + t.strictEqual(client.stats.size, n) + t.strictEqual(client.stats.running, 0) } }) + + await t.completed }) -test('invalid pool dispatch options', (t) => { - t.plan(2) +test('invalid pool dispatch options', async (t) => { + t = tspl(t, { plan: 2 }) const pool = new Pool('http://notahost') t.throws(() => pool.dispatch({}), errors.InvalidArgumentError, 'throws on invalid handler') t.throws(() => pool.dispatch({}, {}), errors.InvalidArgumentError, 'throws on invalid handler') }) -test('pool upgrade promise', (t) => { - t.plan(2) +test('pool upgrade promise', async (t) => { + t = tspl(t, { plan: 2 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -482,11 +505,11 @@ test('pool upgrade promise', (t) => { c.end() }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { headers, socket } = await client.upgrade({ path: '/', @@ -500,20 +523,22 @@ test('pool upgrade promise', (t) => { }) socket.on('close', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) - t.same(headers, { + t.deepStrictEqual(headers, { hello: 'world', connection: 'upgrade', upgrade: 'websocket' }) socket.end() }) + + await t.completed }) -test('pool connect', (t) => { - t.plan(1) +test('pool connect', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((c) => { t.fail() @@ -530,11 +555,11 @@ test('pool connect', (t) => { socket.end(data) }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) const { socket } = await client.connect({ path: '/' @@ -546,25 +571,27 @@ test('pool connect', (t) => { }) socket.on('end', () => { - t.equal(recvData.toString(), 'Body') + t.strictEqual(recvData.toString(), 'Body') }) socket.write('Body') socket.end() }) + + await t.completed }) -test('pool dispatch', (t) => { - t.plan(2) +test('pool dispatch', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) let buf = '' client.dispatch({ @@ -574,22 +601,24 @@ test('pool dispatch', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }, onData (chunk) { buf += chunk }, onComplete () { - t.equal(buf, 'asd') + t.strictEqual(buf, 'asd') }, onError () { } }) }) + + await t.completed }) -test('pool pipeline args validation', (t) => { - t.plan(2) +test('pool pipeline args validation', async (t) => { + t = tspl(t, { plan: 2 }) const client = new Pool('http://localhost:5000') @@ -598,40 +627,44 @@ test('pool pipeline args validation', (t) => { t.ok(/opts/.test(err.message)) t.ok(err instanceof errors.InvalidArgumentError) }) + + await t.completed }) -test('300 requests succeed', (t) => { - t.plan(300 * 3) +test('300 requests succeed', async (t) => { + t = tspl(t, { plan: 300 * 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) for (let n = 0; n < 300; ++n) { client.request({ path: '/', method: 'GET' }, (err, data) => { - t.error(err) + t.ifError(err) data.body.on('data', (chunk) => { - t.equal(chunk.toString(), 'asd') + t.strictEqual(chunk.toString(), 'asd') }).on('end', () => { t.ok(true, 'pass') }) }) } }) + + await t.completed }) -test('pool connect error', (t) => { - t.plan(1) +test('pool connect error', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((c) => { t.fail() @@ -639,11 +672,11 @@ test('pool connect error', (t) => { server.on('connect', (req, socket, firstBodyChunk) => { socket.destroy() }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { await client.connect({ @@ -653,10 +686,12 @@ test('pool connect error', (t) => { t.ok(err) } }) + + await t.completed }) -test('pool upgrade error', (t) => { - t.plan(1) +test('pool upgrade error', async (t) => { + t = tspl(t, { plan: 1 }) const server = net.createServer((c) => { c.on('data', (d) => { @@ -671,11 +706,11 @@ test('pool upgrade error', (t) => { // Ignore error. }) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`) - t.teardown(client.close.bind(client)) + after(() => client.close()) try { await client.upgrade({ @@ -687,22 +722,24 @@ test('pool upgrade error', (t) => { t.ok(err) } }) + + await t.completed }) -test('pool dispatch error', (t) => { - t.plan(3) +test('pool dispatch error', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.dispatch({ path: '/', @@ -711,7 +748,7 @@ test('pool dispatch error', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }, onData (chunk) { }, @@ -739,26 +776,28 @@ test('pool dispatch error', (t) => { t.fail() }, onError (err) { - t.equal(err.code, 'UND_ERR_INVALID_ARG') + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') } }) }) + + await t.completed }) -test('pool request abort in queue', (t) => { - t.plan(3) +test('pool request abort in queue', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.dispatch({ path: '/', @@ -767,7 +806,7 @@ test('pool request abort in queue', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }, onData (chunk) { }, @@ -784,26 +823,28 @@ test('pool request abort in queue', (t) => { method: 'GET', signal }, (err) => { - t.equal(err.code, 'UND_ERR_ABORTED') + t.strictEqual(err.code, 'UND_ERR_ABORTED') }) signal.emit('abort') }) + + await t.completed }) -test('pool stream abort in queue', (t) => { - t.plan(3) +test('pool stream abort in queue', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.dispatch({ path: '/', @@ -812,7 +853,7 @@ test('pool stream abort in queue', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }, onData (chunk) { }, @@ -829,26 +870,28 @@ test('pool stream abort in queue', (t) => { method: 'GET', signal }, ({ body }) => body, (err) => { - t.equal(err.code, 'UND_ERR_ABORTED') + t.strictEqual(err.code, 'UND_ERR_ABORTED') }) signal.emit('abort') }) + + await t.completed }) -test('pool pipeline abort in queue', (t) => { - t.plan(3) +test('pool pipeline abort in queue', async (t) => { + t = tspl(t, { plan: 3 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) client.dispatch({ path: '/', @@ -857,7 +900,7 @@ test('pool pipeline abort in queue', (t) => { onConnect () { }, onHeaders (statusCode, headers) { - t.equal(statusCode, 200) + t.strictEqual(statusCode, 200) }, onData (chunk) { }, @@ -874,26 +917,28 @@ test('pool pipeline abort in queue', (t) => { method: 'GET', signal }, ({ body }) => body).end().on('error', (err) => { - t.equal(err.code, 'UND_ERR_ABORTED') + t.strictEqual(err.code, 'UND_ERR_ABORTED') }) signal.emit('abort') }) + + await t.completed }) -test('pool stream constructor error destroy body', (t) => { - t.plan(4) +test('pool stream constructor error destroy body', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) { const body = new Readable({ @@ -910,8 +955,8 @@ test('pool stream constructor error destroy body', (t) => { }, () => { t.fail() }, (err) => { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(body.destroyed, true) + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + t.strictEqual(body.destroyed, true) }) } @@ -927,27 +972,29 @@ test('pool stream constructor error destroy body', (t) => { }, () => { t.fail() }, (err) => { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(body.destroyed, true) + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + t.strictEqual(body.destroyed, true) }) } }) + + await t.completed }) -test('pool request constructor error destroy body', (t) => { - t.plan(4) +test('pool request constructor error destroy body', async (t) => { + t = tspl(t, { plan: 4 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.close.bind(client)) + after(() => client.close()) { const body = new Readable({ @@ -962,8 +1009,8 @@ test('pool request constructor error destroy body', (t) => { 'transfer-encoding': 'fail' } }, (err) => { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(body.destroyed, true) + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + t.strictEqual(body.destroyed, true) }) } @@ -977,40 +1024,42 @@ test('pool request constructor error destroy body', (t) => { method: 'CONNECT', body }, (err) => { - t.equal(err.code, 'UND_ERR_INVALID_ARG') - t.equal(body.destroyed, true) + t.strictEqual(err.code, 'UND_ERR_INVALID_ARG') + t.strictEqual(body.destroyed, true) }) } }) + + await t.completed }) -test('pool close waits for all requests', (t) => { - t.plan(5) +test('pool close waits for all requests', async (t) => { + t = tspl(t, { plan: 5 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) client.request({ path: '/', method: 'GET' }, (err) => { - t.error(err) + t.ifError(err) }) client.close(() => { @@ -1028,22 +1077,24 @@ test('pool close waits for all requests', (t) => { t.ok(err instanceof errors.ClientClosedError) }) }) + + await t.completed }) -test('pool destroyed', (t) => { - t.plan(1) +test('pool destroyed', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.destroy() client.request({ @@ -1053,43 +1104,45 @@ test('pool destroyed', (t) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) + + await t.completed }) -test('pool destroy fails queued requests', (t) => { - t.plan(6) +test('pool destroy fails queued requests', async (t) => { + t = tspl(t, { plan: 6 }) const server = createServer((req, res) => { res.end('asd') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, async () => { const client = new Pool(`http://localhost:${server.address().port}`, { connections: 1, pipelining: 1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const _err = new Error() client.request({ path: '/', method: 'GET' }, (err) => { - t.equal(err, _err) + t.strictEqual(err, _err) }) client.request({ path: '/', method: 'GET' }, (err) => { - t.equal(err, _err) + t.strictEqual(err, _err) }) - t.equal(client.destroyed, false) + t.strictEqual(client.destroyed, false) client.destroy(_err, () => { t.ok(true, 'pass') }) - t.equal(client.destroyed, true) + t.strictEqual(client.destroyed, true) client.request({ path: '/', @@ -1098,4 +1151,5 @@ test('pool destroy fails queued requests', (t) => { t.ok(err instanceof errors.ClientDestroyedError) }) }) + await t.completed }) diff --git a/test/request-timeout.js b/test/request-timeout.js index 0db4417e0d5..f49f6c5bcf3 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -1,6 +1,7 @@ 'use strict' -const { test } = require('tap') +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createReadStream, writeFileSync, unlinkSync } = require('node:fs') const { Client, errors } = require('..') const { kConnect } = require('../lib/core/symbols') @@ -16,71 +17,75 @@ const { PassThrough } = require('node:stream') -test('request timeout', (t) => { - t.plan(1) +test('request timeout', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { setTimeout(() => { res.end('hello') }, 1000) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) }) }) + + await t.completed }) -test('request timeout with readable body', (t) => { - t.plan(1) +test('request timeout with readable body', async (t) => { + t = tspl(t, { plan: 1 }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) const tempfile = `${__filename}.10mb.txt` writeFileSync(tempfile, Buffer.alloc(10 * 1024 * 1024)) - t.teardown(() => unlinkSync(tempfile)) + after(() => unlinkSync(tempfile)) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 1e3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const body = createReadStream(tempfile) client.request({ path: '/', method: 'POST', body }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) }) }) + + await t.completed }) -test('body timeout', (t) => { - t.plan(2) +test('body timeout', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, { body }) => { - t.error(err) + t.ifError(err) body.on('data', () => { clock.tick(100) }).on('error', (err) => { @@ -90,17 +95,19 @@ test('body timeout', (t) => { clock.tick(50) }) + + await t.completed }) -test('overridden request timeout', (t) => { - t.plan(1) +test('overridden request timeout', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -110,11 +117,11 @@ test('overridden request timeout', (t) => { }, 100) clock.tick(100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 500 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', headersTimeout: 50 }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -122,31 +129,33 @@ test('overridden request timeout', (t) => { clock.tick(50) }) + + await t.completed }) -test('overridden body timeout', (t) => { - t.plan(2) +test('overridden body timeout', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { res.write('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 500 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', bodyTimeout: 50 }, (err, { body }) => { - t.error(err) + t.ifError(err) body.on('data', () => { clock.tick(100) }).on('error', (err) => { @@ -156,17 +165,19 @@ test('overridden body timeout', (t) => { clock.tick(50) }) + + await t.completed }) -test('With EE signal', (t) => { - t.plan(1) +test('With EE signal', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -176,14 +187,14 @@ test('With EE signal', (t) => { }, 100) clock.tick(100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) const ee = new EventEmitter() - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -191,17 +202,19 @@ test('With EE signal', (t) => { clock.tick(50) }) + + await t.completed }) -test('With abort-controller signal', (t) => { - t.plan(1) +test('With abort-controller signal', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -211,14 +224,14 @@ test('With abort-controller signal', (t) => { }, 100) clock.tick(100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) const abortController = new AbortController() - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -226,17 +239,19 @@ test('With abort-controller signal', (t) => { clock.tick(50) }) + + await t.completed }) -test('Abort before timeout (EE)', (t) => { - t.plan(1) +test('Abort before timeout (EE)', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -248,30 +263,32 @@ test('Abort before timeout (EE)', (t) => { ee.emit('abort') clock.tick(50) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', signal: ee }, (err, response) => { t.ok(err instanceof errors.RequestAbortedError) clock.tick(100) }) }) + + await t.completed }) -test('Abort before timeout (abort-controller)', (t) => { - t.plan(1) +test('Abort before timeout (abort-controller)', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -283,30 +300,32 @@ test('Abort before timeout (abort-controller)', (t) => { abortController.abort() clock.tick(50) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET', signal: abortController.signal }, (err, response) => { t.ok(err instanceof errors.RequestAbortedError) clock.tick(100) }) }) + + await t.completed }) -test('Timeout with pipelining', (t) => { - t.plan(3) +test('Timeout with pipelining', async (t) => { + t = tspl(t, { plan: 3 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -316,14 +335,14 @@ test('Timeout with pipelining', (t) => { }, 100) clock.tick(50) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 10, headersTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -337,17 +356,19 @@ test('Timeout with pipelining', (t) => { t.ok(err instanceof errors.HeadersTimeoutError) }) }) + + await t.completed }) -test('Global option', (t) => { - t.plan(1) +test('Global option', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -357,13 +378,13 @@ test('Global option', (t) => { }, 100) clock.tick(100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -371,17 +392,19 @@ test('Global option', (t) => { clock.tick(50) }) + + await t.completed }) -test('Request options overrides global option', (t) => { - t.plan(1) +test('Request options overrides global option', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -391,13 +414,13 @@ test('Request options overrides global option', (t) => { }, 100) clock.tick(100) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 50 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) @@ -405,15 +428,17 @@ test('Request options overrides global option', (t) => { clock.tick(50) }) + + await t.completed }) -test('client.destroy should cancel the timeout', (t) => { - t.plan(2) +test('client.destroy should cancel the timeout', async (t) => { + t = tspl(t, { plan: 2 }) const server = createServer((req, res) => { res.end('hello') }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { @@ -425,39 +450,41 @@ test('client.destroy should cancel the timeout', (t) => { }) client.destroy(err => { - t.error(err) + t.ifError(err) }) }) + + await t.completed }) -test('client.close should wait for the timeout', (t) => { - t.plan(2) +test('client.close should wait for the timeout', async (t) => { + t = tspl(t, { plan: 2 }) const clock = FakeTimers.install({ shouldClearNativeTimers: true }) - t.teardown(clock.uninstall.bind(clock)) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 100 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { t.ok(err instanceof errors.HeadersTimeoutError) }) client.close((err) => { - t.error(err) + t.ifError(err) }) client.on('connect', () => { @@ -466,16 +493,18 @@ test('client.close should wait for the timeout', (t) => { }) }) }) + + await t.completed }) -test('Validation', (t) => { - t.plan(4) +test('Validation', async (t) => { + t = tspl(t, { plan: 4 }) try { const client = new Client('http://localhost:3000', { headersTimeout: 'foobar' }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) } @@ -484,7 +513,7 @@ test('Validation', (t) => { const client = new Client('http://localhost:3000', { headersTimeout: -1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) } @@ -493,7 +522,7 @@ test('Validation', (t) => { const client = new Client('http://localhost:3000', { bodyTimeout: 'foobar' }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) } @@ -502,21 +531,23 @@ test('Validation', (t) => { const client = new Client('http://localhost:3000', { bodyTimeout: -1 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) } catch (err) { t.ok(err instanceof errors.InvalidArgumentError) } + + await t.completed }) -test('Disable request timeout', (t) => { - t.plan(2) +test('Disable request timeout', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -526,39 +557,41 @@ test('Disable request timeout', (t) => { }, 32e3) clock.tick(33e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 0, connectTimeout: 0 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.error(err) + t.ifError(err) const bufs = [] response.body.on('data', (buf) => { bufs.push(buf) }) response.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) clock.tick(31e3) }) + + await t.completed }) -test('Disable request timeout for a single request', (t) => { - t.plan(2) +test('Disable request timeout for a single request', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -568,39 +601,41 @@ test('Disable request timeout for a single request', (t) => { }, 32e3) clock.tick(33e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 0, connectTimeout: 0 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.request({ path: '/', method: 'GET' }, (err, response) => { - t.error(err) + t.ifError(err) const bufs = [] response.body.on('data', (buf) => { bufs.push(buf) }) response.body.on('end', () => { - t.equal('hello', Buffer.concat(bufs).toString('utf8')) + t.strictEqual('hello', Buffer.concat(bufs).toString('utf8')) }) }) clock.tick(31e3) }) + + await t.completed }) -test('stream timeout', (t) => { - t.plan(1) +test('stream timeout', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -610,11 +645,11 @@ test('stream timeout', (t) => { }, 301e3) clock.tick(301e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { connectTimeout: 0 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.stream({ path: '/', @@ -626,17 +661,19 @@ test('stream timeout', (t) => { t.ok(err instanceof errors.HeadersTimeoutError) }) }) + + await t.completed }) -test('stream custom timeout', (t) => { - t.plan(1) +test('stream custom timeout', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -646,13 +683,13 @@ test('stream custom timeout', (t) => { }, 31e3) clock.tick(31e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 30e3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client.stream({ path: '/', @@ -664,17 +701,19 @@ test('stream custom timeout', (t) => { t.ok(err instanceof errors.HeadersTimeoutError) }) }) + + await t.completed }) -test('pipeline timeout', (t) => { - t.plan(1) +test('pipeline timeout', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -684,11 +723,11 @@ test('pipeline timeout', (t) => { }, 301e3) clock.tick(301e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const buf = Buffer.alloc(1e6).toString() pipeline( @@ -719,17 +758,19 @@ test('pipeline timeout', (t) => { } ) }) + + await t.completed }) -test('pipeline timeout', (t) => { - t.plan(1) +test('pipeline timeout', async (t) => { + t = tspl(t, { plan: 1 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) @@ -739,13 +780,13 @@ test('pipeline timeout', (t) => { }, 31e3) clock.tick(31e3) }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { headersTimeout: 30e3 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) const buf = Buffer.alloc(1e6).toString() pipeline( @@ -776,30 +817,32 @@ test('pipeline timeout', (t) => { } ) }) + + await t.completed }) -test('client.close should not deadlock', (t) => { - t.plan(2) +test('client.close should not deadlock', async (t) => { + t = tspl(t, { plan: 2 }) - const clock = FakeTimers.install() - t.teardown(clock.uninstall.bind(clock)) + const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + after(() => clock.uninstall()) const orgTimers = { ...timers } Object.assign(timers, { setTimeout, clearTimeout }) - t.teardown(() => { + after(() => { Object.assign(timers, orgTimers) }) const server = createServer((req, res) => { }) - t.teardown(server.close.bind(server)) + after(() => server.close()) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`, { bodyTimeout: 200, headersTimeout: 100 }) - t.teardown(client.destroy.bind(client)) + after(() => client.destroy()) client[kConnect](() => { client.request({ @@ -810,10 +853,11 @@ test('client.close should not deadlock', (t) => { }) client.close((err) => { - t.error(err) + t.ifError(err) }) clock.tick(100) }) }) + await t.completed }) diff --git a/test/tls-session-reuse.js b/test/tls-session-reuse.js index 0c784456798..c70578bed1f 100644 --- a/test/tls-session-reuse.js +++ b/test/tls-session-reuse.js @@ -1,10 +1,11 @@ 'use strict' +const { tspl } = require('@matteo.collina/tspl') +const { test, after, describe } = require('node:test') const { readFileSync } = require('node:fs') const { join } = require('node:path') const https = require('node:https') const crypto = require('node:crypto') -const { test, teardown } = require('tap') const { Client, Pool } = require('..') const { kSocket } = require('../lib/core/symbols') @@ -14,12 +15,12 @@ const options = { } const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') -test('A client should disable session caching', t => { +describe('A client should disable session caching', () => { const clientSessions = {} let serverRequests = 0 - t.test('Prepare request', t => { - t.plan(3) + test('Prepare request', async t => { + t = tspl(t, { plan: 3 }) const server = https.createServer(options, (req, res) => { if (req.url === '/drop-key') { server.setTicketKeys(crypto.randomBytes(48)) @@ -40,7 +41,7 @@ test('A client should disable session caching', t => { maxCachedSessions: 0 }) - t.teardown(() => { + after(() => { client.close() server.close() }) @@ -64,7 +65,7 @@ test('A client should disable session caching', t => { delete tls.ciphers } client.request(options, (err, data) => { - t.error(err) + t.ifError(err) clientSessions[options.name] = client[kSocket].getSession() data.body.resume().on('end', () => { if (queue.length !== 0) { @@ -76,28 +77,29 @@ test('A client should disable session caching', t => { } request() }) + + await t.completed }) - t.test('Verify cached sessions', t => { - t.plan(2) - t.equal(serverRequests, 2) - t.not( + test('Verify cached sessions', async t => { + t = tspl(t, { plan: 2 }) + t.strictEqual(serverRequests, 2) + t.notEqual( clientSessions.first.toString('hex'), clientSessions.second.toString('hex') ) + await t.completed }) - - t.end() }) -test('A pool should be able to reuse TLS sessions between clients', t => { +describe('A pool should be able to reuse TLS sessions between clients', () => { let serverRequests = 0 const REQ_COUNT = 10 const ASSERT_PERFORMANCE_GAIN = false - t.test('Prepare request', t => { - t.plan(2 + 1 + (ASSERT_PERFORMANCE_GAIN ? 1 : 0)) + test('Prepare request', async t => { + t = tspl(t, { plan: 2 + 1 + (ASSERT_PERFORMANCE_GAIN ? 1 : 0) }) const server = https.createServer(options, (req, res) => { serverRequests++ res.end() @@ -137,7 +139,7 @@ test('A pool should be able to reuse TLS sessions between clients', t => { numSessions++ }) - t.teardown(() => { + after(() => { poolWithSessionReuse.close() poolWithoutSessionReuse.close() server.close() @@ -168,13 +170,11 @@ test('A pool should be able to reuse TLS sessions between clients', t => { await runRequests(poolWithoutSessionReuse, REQ_COUNT, false) await runRequests(poolWithSessionReuse, REQ_COUNT, true) - t.equal(numSessions, 2) - t.equal(serverRequests, 2 + REQ_COUNT * 2) + t.strictEqual(numSessions, 2) + t.strictEqual(serverRequests, 2 + REQ_COUNT * 2) t.ok(true, 'pass') }) - }) - t.end() + await t.completed + }) }) - -teardown(() => process.exit()) From d076328204b591986f5d252c2b338bc4a8812a9a Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 13 Feb 2024 22:30:03 +0100 Subject: [PATCH 028/123] chore: remove usage of http-errors in proxy example (#2753) --- examples/proxy/proxy.js | 68 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/examples/proxy/proxy.js b/examples/proxy/proxy.js index 6c16a5e70b0..8826bc722ce 100644 --- a/examples/proxy/proxy.js +++ b/examples/proxy/proxy.js @@ -1,6 +1,8 @@ +'use strict' + const net = require('node:net') const { pipeline } = require('node:stream') -const createError = require('http-errors') +const { STATUS_CODES } = require('node:http') module.exports = async function proxy (ctx, client) { const { req, socket, proxyName } = ctx @@ -214,13 +216,13 @@ function getHeaders ({ ].join(';')) } else if (forwarded) { // The forwarded header should not be included in response. - throw new createError.BadGateway() + throw new BadGateway() } if (proxyName) { if (via) { if (via.split(',').some(name => name.endsWith(proxyName))) { - throw new createError.LoopDetected() + throw new LoopDetected() } via += ', ' } @@ -254,3 +256,63 @@ function printIp (address, port) { } return str } + +class BadGateway extends Error { + constructor (message = STATUS_CODES[502]) { + super(message) + } + + toString () { + return `BadGatewayError: ${this.message}` + } + + get name () { + return 'BadGatewayError' + } + + get status () { + return 502 + } + + get statusCode () { + return 502 + } + + get expose () { + return false + } + + get headers () { + return undefined + } +} + +class LoopDetected extends Error { + constructor (message = STATUS_CODES[508]) { + super(message) + } + + toString () { + return `LoopDetectedError: ${this.message}` + } + + get name () { + return 'LoopDetectedError' + } + + get status () { + return 508 + } + + get statusCode () { + return 508 + } + + get expose () { + return false + } + + get headers () { + return undefined + } +} From 64b133cf56a2d5ca772cdb1f919265dcd95af484 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 14 Feb 2024 07:43:19 +0100 Subject: [PATCH 029/123] fix: dont package wasm files of llhttp with npm (#2752) --- .npmignore | 10 ++++++++++ package.json | 9 --------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.npmignore b/.npmignore index 344e7f65bc9..c55b19ffd15 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,12 @@ +* +!lib/**/* +!index.js +!index-fetch.js + +# The wasm files are stored as base64 strings in the corresponding .js files lib/llhttp/llhttp_simd.wasm lib/llhttp/llhttp.wasm + +!types/**/* +!index.d.ts +!docs/**/* diff --git a/package.json b/package.json index d03da7e116d..0c42aa0696f 100644 --- a/package.json +++ b/package.json @@ -61,15 +61,6 @@ ], "main": "index.js", "types": "index.d.ts", - "files": [ - "*.d.ts", - "index.js", - "index-fetch.js", - "loader.js", - "lib", - "types", - "docs" - ], "scripts": { "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js", "prebuild:wasm": "node build/wasm.js --prebuild", From 954bfd962cf41a2130f0f3f3295816bcd85fe37c Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 14 Feb 2024 12:13:38 +0100 Subject: [PATCH 030/123] fix: handle request body as late as possible (#2734) --- lib/client.js | 36 +++++++++++++++++++++++++++++++++--- lib/core/request.js | 19 ------------------- lib/core/util.js | 4 ++-- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/client.js b/lib/client.js index 98adffdf182..b74ffcbf7be 100644 --- a/lib/client.js +++ b/lib/client.js @@ -105,6 +105,8 @@ const { // Experimental let h2ExperimentalWarned = false +let extractBody + const FastBuffer = Buffer[Symbol.species] const kClosedResolve = Symbol('kClosedResolve') @@ -1446,7 +1448,7 @@ function _resume (client, sync) { } if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && - (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { // Request with stream or iterator body can error while other requests // are inflight and indirectly error those as well. // Ensure this doesn't happen by waiting for inflight @@ -1477,7 +1479,9 @@ function write (client, request) { return } - const { body, method, path, host, upgrade, headers, blocking, reset } = request + const { method, path, host, upgrade, blocking, reset } = request + + let { body, headers, contentLength } = request // https://tools.ietf.org/html/rfc7231#section-4.3.1 // https://tools.ietf.org/html/rfc7231#section-4.3.2 @@ -1494,6 +1498,21 @@ function write (client, request) { method === 'PATCH' ) + if (util.isFormDataLike(body)) { + if (!extractBody) { + extractBody = require('./fetch/body.js').extractBody + } + + const [bodyStream, contentType] = extractBody(body) + if (request.contentType == null) { + headers += `content-type: ${contentType}\r\n` + } + body = bodyStream.stream + contentLength = bodyStream.length + } else if (util.isBlobLike(body) && request.contentType == null && body.type) { + headers += `content-type: ${body.type}\r\n` + } + if (body && typeof body.read === 'function') { // Try to read EOF in order to get length. body.read(0) @@ -1501,7 +1520,7 @@ function write (client, request) { const bodyLength = util.bodyLength(body) - let contentLength = bodyLength + contentLength = bodyLength ?? contentLength if (contentLength === null) { contentLength = request.contentLength @@ -1544,6 +1563,7 @@ function write (client, request) { } if (request.aborted) { + util.destroy(body) return false } @@ -2050,6 +2070,16 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, socket .on('drain', onDrain) .on('error', onFinished) + + if (body.errorEmitted ?? body.errored) { + setImmediate(() => onFinished(body.errored)) + } else if (body.endEmitted ?? body.readableEnded) { + setImmediate(() => onFinished(null)) + } + + if (body.closeEmitted ?? body.closed) { + setImmediate(onClose) + } } async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { diff --git a/lib/core/request.js b/lib/core/request.js index bee7a47af92..16a1efffe61 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -26,8 +26,6 @@ const invalidPathRegex = /[^\u0021-\u00ff]/ const kHandler = Symbol('handler') -let extractBody - class Request { constructor (origin, { path, @@ -182,23 +180,6 @@ class Request { throw new InvalidArgumentError('headers must be an object or an array') } - if (util.isFormDataLike(this.body)) { - if (!extractBody) { - extractBody = require('../fetch/body.js').extractBody - } - - const [bodyStream, contentType] = extractBody(body) - if (this.contentType == null) { - this.contentType = contentType - this.headers += `content-type: ${contentType}\r\n` - } - this.body = bodyStream.stream - this.contentLength = bodyStream.length - } else if (util.isBlobLike(body) && this.contentType == null && body.type) { - this.contentType = body.type - this.headers += `content-type: ${body.type}\r\n` - } - util.validateHandler(handler, method, upgrade) this.servername = util.getServerName(this.host) diff --git a/lib/core/util.js b/lib/core/util.js index 7b863067870..1ad0eab89fd 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -182,8 +182,8 @@ function bodyLength (body) { return null } -function isDestroyed (stream) { - return !stream || !!(stream.destroyed || stream[kDestroyed]) +function isDestroyed (body) { + return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body))) } function isReadableAborted (stream) { From c5f069a80e9fe82b8944db2d1b58064d3a846462 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:24:00 +0900 Subject: [PATCH 031/123] perf(tree): avoid recursive calls (#2755) --- lib/core/tree.js | 50 ++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 03be18d235e..366fc7d3207 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -36,31 +36,39 @@ class TstNode { /** * @param {Uint8Array} key * @param {any} value - * @param {number} index */ - add (key, value, index) { - if (index === undefined || index >= key.length) { + add (key, value) { + const length = key.length + if (length === 0) { throw new TypeError('Unreachable') } - const code = key[index] - if (this.code === code) { - if (key.length === ++index) { - this.value = value - } else if (this.middle !== null) { - this.middle.add(key, value, index) - } else { - this.middle = new TstNode(key, value, index) - } - } else if (this.code < code) { - if (this.left !== null) { - this.left.add(key, value, index) + let index = 0 + let node = this + while (true) { + const code = key[index] + if (node.code === code) { + if (length === ++index) { + node.value = value + break + } else if (node.middle !== null) { + node = node.middle + } else { + node.middle = new TstNode(key, value, index) + break + } + } else if (node.code < code) { + if (node.left !== null) { + node = node.left + } else { + node.left = new TstNode(key, value, index) + break + } + } else if (node.right !== null) { + node = node.right } else { - this.left = new TstNode(key, value, index) + node.right = new TstNode(key, value, index) + break } - } else if (this.right !== null) { - this.right.add(key, value, index) - } else { - this.right = new TstNode(key, value, index) } } @@ -107,7 +115,7 @@ class TernarySearchTree { if (this.node === null) { this.node = new TstNode(key, value, 0) } else { - this.node.add(key, value, 0) + this.node.add(key, value) } } From eb884c5f792345e4f13d0e555a68ebb06020b796 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 14 Feb 2024 14:27:13 +0100 Subject: [PATCH 032/123] docs: fix favicon (#2758) --- index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.html b/index.html index 672a592a0d3..894ce1b875e 100644 --- a/index.html +++ b/index.html @@ -7,8 +7,7 @@ - - + From edd22bcbf005d48a6e0c9a52884207dac73fc232 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 14 Feb 2024 16:38:53 +0100 Subject: [PATCH 033/123] chore: use mermaid engine and mermaid in markdown (#2759) --- docs/api/api-lifecycle.md | 37 ++++++++++++++++++++++++++---- docs/assets/lifecycle-diagram.png | Bin 47090 -> 0 bytes index.html | 32 ++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 6 deletions(-) delete mode 100644 docs/assets/lifecycle-diagram.png diff --git a/docs/api/api-lifecycle.md b/docs/api/api-lifecycle.md index d158126d0c5..2e7db25d132 100644 --- a/docs/api/api-lifecycle.md +++ b/docs/api/api-lifecycle.md @@ -21,10 +21,39 @@ An Undici [Client](Client.md) can be best described as a state machine. The foll * At any point in time, the *destroy* event will transition the `Client` from the **processing** state to the **destroyed** state, destroying any queued requests. * The **destroyed** state is a final state and the `Client` is no longer functional. -![A state diagram representing an Undici Client instance](../assets/lifecycle-diagram.png) - -> The diagram was generated using Mermaid.js Live Editor. Modify the state diagram [here](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgWypdIC0tPiBpZGxlXG4gICAgaWRsZSAtLT4gcGVuZGluZyA6IGNvbm5lY3RcbiAgICBpZGxlIC0tPiBkZXN0cm95ZWQgOiBkZXN0cm95L2Nsb3NlXG4gICAgXG4gICAgcGVuZGluZyAtLT4gaWRsZSA6IHRpbWVvdXRcbiAgICBwZW5kaW5nIC0tPiBkZXN0cm95ZWQgOiBkZXN0cm95XG5cbiAgICBzdGF0ZSBjbG9zZV9mb3JrIDw8Zm9yaz4-XG4gICAgcGVuZGluZyAtLT4gY2xvc2VfZm9yayA6IGNsb3NlXG4gICAgY2xvc2VfZm9yayAtLT4gcHJvY2Vzc2luZ1xuICAgIGNsb3NlX2ZvcmsgLS0-IGRlc3Ryb3llZFxuXG4gICAgcGVuZGluZyAtLT4gcHJvY2Vzc2luZyA6IHByb2Nlc3NcblxuICAgIHByb2Nlc3NpbmcgLS0-IHBlbmRpbmcgOiBrZWVwYWxpdmVcbiAgICBwcm9jZXNzaW5nIC0tPiBkZXN0cm95ZWQgOiBkb25lXG4gICAgcHJvY2Vzc2luZyAtLT4gZGVzdHJveWVkIDogZGVzdHJveVxuXG4gICAgc3RhdGUgcHJvY2Vzc2luZyB7XG4gICAgICAgIHJ1bm5pbmcgLS0-IGJ1c3kgOiBuZWVkRHJhaW5cbiAgICAgICAgYnVzeSAtLT4gcnVubmluZyA6IGRyYWluQ29tcGxldGVcbiAgICAgICAgcnVubmluZyAtLT4gWypdIDoga2VlcGFsaXZlXG4gICAgICAgIHJ1bm5pbmcgLS0-IGNsb3NpbmcgOiBjbG9zZVxuICAgICAgICBjbG9zaW5nIC0tPiBbKl0gOiBkb25lXG4gICAgICAgIFsqXSAtLT4gcnVubmluZ1xuICAgIH1cbiAgICAiLCJtZXJtYWlkIjp7InRoZW1lIjoiYmFzZSJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ) - +A state diagram representing an Undici Client instance: + +```mermaid +stateDiagram-v2 + [*] --> idle + idle --> pending : connect + idle --> destroyed : destroy/close + + pending --> idle : timeout + pending --> destroyed : destroy + + state close_fork <> + pending --> close_fork : close + close_fork --> processing + close_fork --> destroyed + + pending --> processing : process + + processing --> pending : keepalive + processing --> destroyed : done + processing --> destroyed : destroy + + destroyed --> [*] + + state processing { + [*] --> running + running --> closing : close + running --> busy : needDrain + busy --> running : drainComplete + running --> [*] : keepalive + closing --> [*] : done + } +``` ## State details ### idle diff --git a/docs/assets/lifecycle-diagram.png b/docs/assets/lifecycle-diagram.png deleted file mode 100644 index 4ef17b5a71454bdd5d0920bdbc162d24347f7440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47090 zcmZU*1yoeq8#fF{Hx8ksl!PFuGy@1o51rB=ph!s9ASDeF(lVrgN{4heNJ~n0cMtGx z?*CoiTHp8GyVhL`an78x&$FNBSNn#ls=Ow^rNl)+K_QTrlU7GTc|eAOf)77|WSqn9kuEkOQ^8shB{%<&PXs6i88>kGQu%-5ivHdKJ7(Ssz z&RL@2M5vQ~WX@){N@T99s{32!y6Zj9_~u~QcTb2O4dvkRDGExn)dmX+$_+Y71_A}; z1cLGjj)wAxs1jwrroLWOP;hc~cK6it27b4<_Z+2JFkE%_?9A2NoKbl+i#3+XhM#iR z)$8NOk6vCPXwB8QBz@!O3KQs(B86Nf8TLFe;y^9jg3-#0_h(d%(F?+=`ORi zv}8tW7P!JWJ3G@a)V@{xrn0`V!HU+bbA==PZD9MOPVSVgYuzk1$`y`^ib_~kH}r{o zY+ha-Hp+4i`2tJZVu|+8AfoPLBoZl#(wsDZhYo>2P*L(9w!pDa5F`xsmY$whv&YMk zvgS56Z2w)?vtjP_9@obCH3>@NFs!7cC>#M3< zy}iACed$ma46ksis;bV;&wa2^^081*#9u*CQ8rj^Bg#1J?d;G{h+8gMP#kV?P@3Ug z^KOogYCr$p{~@^VGZ7S&YnuPoe+Y&G9{vCOR)%2iO&BQk^JsJSs3`ltQBWrL|DQKD z1Uoo7Izl3;Q9fbcHa$SOn@2@KKL3ATP6FC;dg_wCj*1epaEpfG;ERg#>4p$R;(s@2 z9~~W?J`*Sk6qtARBA%6DubAtCWeh!9+g=c4Wh3-A8^=z%|l z!C({=6lrNwvMIZlvf6U6y&o}D``&(pqe+e6wH5LWMD8+~)d2Fwko3^Wk&#CL2 zPA3UD)=eiBOp?;lIh#oc?4GB++n(MFZSBM?Uuhjti5NtSnPe`p85kJ;ble26(IbQ8 zqORtlisbB?a+#jcZPKXdLk(8k8{O(0|B#S;6XG0YbqHg&&yGymYa`mt;Xy{x; zc;EsA?S~>!|HQgVD0!7>=?+ne;8fuA~nDrlVSJ6 zrezkOGW0TYZNXvq7$hWW;%0F035Ngwf3xgY?n2PeUMamy!Nd$^pX0ON>@*6<>pj+{ z2`-#2(E-obj+9k)tYROOI_t)PJA!}DcJsw%pQ zrsa(LHyI1QXm{D(W9MJ`gK^Wv823jH$$8pM_P*1Q@4<{0^U7!VI)a&=GDoMC47uOT z8JwpP(v-hQYwC2Lr3DWe^KykHi`Gz|E6FCqyZXcLuf7ron9EaLgmxuDWrc5&ABGX} z@Ieoexf4eiaa?Ktd}dvJ>&E}U(i@JDxq09%$o)KmsJ-#_pUpMsRw_&pXl_HmuSi_f z!iY*s>u&qFpm&#D#e@Uk0|&oUi%2Pr>yNbWD@5x?aG2`O1ZjKY2I*iiWiXO{TRMi` zMrJQ_WdEo)G(@fsTsKn+R}U@sB8LusfeL+=z~$pUum9goN6(XxO*{)jBo@#2b$6@Q zV?A({q+b-g+^e(ysmXrb+t$@3AaP%2e=b8ee4XK&GudLF*RSxu**UK4EH1kW*hAID zXU^uo6H)(j-4zKtW?RNwUUzRJyXH{QrxBU3Pg*S1pdbU)uOAC`TMsluYJOoo{xG1< z71=ex<#4QmH}*M^z2N%he7+}simzSZ3zDbmqn(ZDI>vB=P5(N6 z$;ik^I0LG|(-fXH^V=Sl!oOMEbW17pait@i;@AgSWZVQ7PQLFt=A99VT*mhmOmOne zA2$umitOZ((NQ(SlZ(S;2AMF{vF3C!uauV`wzjv!<<5@>p>BKsYP1XWZ?4bxXMPvo z-*f;ryF}5O8eY$n4e0HKd28VPtb3;mjS_Yo6V1d^AdfIGFgV{F&GWuI!XxK!aC4&o zufii>*==Qg{kPq0EhYhFh21QrkLd95@Diih18Q}~^p#g|Tt^jbujjm(Ce)WUK8B=7% z2Ofs!{)L+A$t@i@N4|DpihFcqWMpF_yeF0!bm7n`fA9#bzqz5qFx20V|7x+7?0=jl z;`V%v=)N$xI+4;{(R7IzuMF#4U7VaCLWz$yA1}MHaBg|f2IYlxsm%$*nLp^5AxMkrAX*&c$7nmQs#fXA^|>!-Ge>keo{#Q(SFYinzF z6XX{fHF|qIf1VT{lb-O9B8_zS84=a{@`jlR$Jn32#tPm^-C_cE{~kf%Nc*On&uWTk zp!;V1NLHTtjK!)a%-6#OVMuG{2wUUc90s*uvOw6wYp z3cw5$^Ezw)5zA1#@alCWB{RRawKeOHY)S#Uh|1#r{{Dv#AEN)S(G@KQURqlAC3H~! zn|#k4EzsRYOpjX;v{nboEQExyq*~}@prf-hDOdmuE~uKET=!=Js*+wcu$YOQ1_?1S z^hea*U{4AdbQ4M z_SW#x{}Hn7niPWN_oVbyVMZ*D3lakupJ$b(O;dcm^* z$}n-pzphM9N$J&dd1nc}|5Y&Is%2PYWJd;j7E^D3@tYb3@LG?Up~G_xUa9;x{+T>_ zD>>;UIF935-!~Ro0wzsMK)ru7e+w$6z8gjgfdu@p*s12V=qC2^@_I2J>AukH=PzC{ zCwaa%OZC#W+Uwko?2Pgu2?^M}t9Bedp$e3m%yqT3xRn!iZuTn6cYz{qdl~NtwrQRa zn}34c-zpg1P-xTT@w)l3zEW}9xQi4dFgVzJOHW@vB?I=+$thLP(Jb^Kvv$#&P%dTg z2_GN0*~b)c9XelRsl%d_udi>WkV?8}PfHzjVS|mebx3>YbJ-nr4)AP^PoQcN63w9F zeo5oLHiVr2`^IObqB7CXWN*K(SN@iaP2J-1XjMp;H$5Z-;2i-K+XwzM+a-Hus;Xax zlqNYZ!V`VW!cut5F>z->hy42G%ai|hf_TF;)7f!Xq|;h|@}n9^$}dpl#zG6-Bw|XW z&FSqsos0}utZOXO{^}CbazV~c$t^ZE-q7UN2lDwAKODnT(-t|1O!0e1W-gz zu`u<;`_Eqs=)}EQal=4IWPbV-x0gV71FudJyyoeF0Tp7Bn1pqfTCm20>0*w?qQ1BF zthyyf)$F}<$>pb~r!~>EBr#`eEYFp=J&r6$adzjMj7g>MkjK&4+1VNY?xE*MuA;do zBSko<@FJx2q}KT$^sb%ybY;atO+xvb@aa(ID}T-MV4@=puquE0`@y!fc?DnUw3<9| zTj`ETGGx}|dKEHOffS+@brv-@PyMFzJs0l^jgXM=zP=1K$Km4M$ptoB9xKR?5Ksnj z#l`!e_)}kgc=!JOvmCT<2($nx0sE<wK?+Q@I9SQMJlKZBVs$aB$+0$HT++ zx7+$})VDgP^Un%g#*J@ETf1%NTW}yOv$Z|~+ipcaf9ljaCnK-;1U<(*k5*L!s`;fX6(1YZv<8B&+F#k^N^$M=zy z!M}s^+~7Bl`Rzt>^vc%|wf|kod9gJJOh0oEH2MNbc&+p0R;x^SFcDqN+dslEt||J! zFBGMAvzYu#1z1GX7R$>lgOQQSt*zMy3Zj)Ca~-8EBD`@QNVx6IHGMq28Y>ums+8n@ z+52LZfcLq$&3chowrnI1f12Y}h4Q%9dZ)5M*Q1DzVkwhauYFYgwf=9xRNj3Fg$W#n zRrYt1bE-~i4sw&tl2;d=+kfr3ZARkXPuMTv8Q0nhHNJDgQ}pn0w0saCH59U4X}RtC zrp~Eqokb3KsQEu*{p8+AtezjsHA1c$&j=X_mH}ou`!Ci;NGi_d&92{|YSrZ3`w{Z@giZ zH*=A{?dDuVaDC%ZUl()2gpT;n`tNdokEZDv`gG7#w#EyoXduz9?e8z2U!Dj2d<;o_ z5{wN>E4v@o-Q|wUGMY!#J0H;(8E_dIvccI8n|w}T?h2qBE;crC2JNN%9M>rpk-VHz z%b$Me6YSr*X&J{TyJbY1Q^z{I+>4lO#?jqh(_*92L!)R!78j}RB$5#h&s-dRBAn_; zcIBXg2oZbnf#HUj_%tvq*sh^_^S&$F>sqE?1BE5wtHUVs&8Iet_QCpL7j`^^b>{2ob?3C~$D<+>6uz|8u7WXK+HcM9cY8RPi`g)b4odDm z`BncC$fL(Aed9n+D z0mlP3WPEuRNKY1Mj~YGxuBqr=9nn-nm-8GFL(tK|L3=#5c)nz+YXUlW1UZI84VB1o zSp3M-L6%?Q1n+pphpyM1<3HwAI(yM8F1Om>nDXNN8pnTM4RQ5&ru}OYj4+IRW4Kf$ z#Lk`d8Qcm3mH+nU9NL>;*{73%vDzv02euIs+reY)vHm59*UEN)&43$jRpwb3kc~I3 zR#8+-1)lm6FEQS@uGqL4V5{LO!@8YymP;!LDfLy$`!FGN!Ngp9_ltSoLQA;RYf>Nr z46pM*cjp@FfbcLGs`N>JEl;m}y?cpiHIhI};UZN0K}B|}{7VMKMf%U?A5#z(B1Xq6 zo8fMIm2xW9uQRpAqh$-p-enF&HR5XhQ%yz`8P)j}Q$V)MjKY^X!Y2weC=x|dFXabf z+No>Zir73^hok8AFNq};8f9*A=%v_11Rb9r$9dx-{C^Jiao$hRDln=tBp-0-vq~5q z$Nf@8{l}*K=PcXUv^_-aB*Ty`R7989qJjgkqn6tgxRfv6(sh26(goM=oe4JGCi>@rzE2)u1n@%NwX=Lhlp* zAOjyiuudd$+~tnUFup@ATUQ?#Ym*`yA7@$lCr0{pxj;F!YJs}^OqC{Ru(gI4x>$@x z#6z;5QL}o36@5S9KES7|J{W*TS>MnQL>aeDk#R*OB>$cD1ijnePBY4Wu1tB3(bBzD zH{#U|>}zr=QzJbjg5@cVh88e?x4~yMP^eq8bw%JMS(AcT z3Hx46tsBU&?Y6hF^VY_N>GcF@pJY(r=caku$7JE}uUgKCZV9?cgCdG@m)-7L006en zb&CEicSU1ZOTg8)AB-MG=oHevv`uPiYRdT6149UxFpxGn5llmq0RB?57JvP^i^sgv zq}Xd`C@Z7*dtRP^*Vzuc){l%>d^`zOxeo5xMX+@M%Nn7kL?Fz}%)kUIaa`^MaEp!g zy#JfX#P~S*HKyy{ze@m9an+L*$ez_!SF`@dQVD{0^~gE(ndB_{|NMETQ*_UMOG>yU zfB50VdhpxChlJ?fRXD8=JU6VxRxhotuExM4kLS>P$VZJ)&ynpaG}3R2vuHx zn>iRloGhN@jiDicQvi>-ZjG@?*7O2K|M!O$pg@|Dmo@1G@l_hp%REVb2q5fO*hY;{vdV%qlq}gfkEHRP9KUmD5G_0FG1Oftnuf zy`Lz;>D{0G$gY|H=`tFCI>-r`>&~PfPW1hE1z+O)B#WSYq40%LPC-Gx&hq ze;B`{G5TU?<)ySVdpnYpj?VbI(&I|~cWGwk=73ik);imq$kf!;;q}sUly%%{GBA*t z!Sw-#Y!8KC6j)XmHo?UbgqpM7obS(5#EWfiZu;I{vhd#19x_vStA3Q7ot^)fmaHu1 z507^fMPeq+pSCB8@vhPo6Q5_^0eTS=#tqfe)is$ylS~1t@jyBC$d8C0&7GO@gwyo3C`a@nNe&#U@GJwGVji>mbc!%ywP>7P1WbaZr_JCSV&esUjxMcR=$wLNbhU6E)mm^3K=dKzuYvGd*S z^2jptg(|^YpvZ%YaY;hpI@5@i0P=G-2qYWAO*U_PgBuDwh zTGC|nYNN38hQq-^OME=(q0q~hFH_3;ly9z1*Z-ul{O`KY-&B8kXf8R8)t5{7m^Vi{ z^kMniKfud)NVmSV6-M?vaJuas4Aedl$=Z?eSnu(l8`bN6scPL3CB-+zrw~(T4Nz28 zCZqE&@P2fwaco1(N(ELt9xDma8S&WZ;=t616OiAy|EsGErtj|TaLj!cXBQJ|$j@iZ zOaPVtx$;$w11pIdEhwCdUx4@aI57Zb1z?yd#sC<)riz;t75uBj=}}RH3f}|uxBk@} z4}22=Z-13wEw9Y7?{iQPPS$zVUn+iURZ!l5O)W1i`I8$O8cOH8;b!uVsK;jcEWe`d z!l+xFSactqp4*ccnBPc$w3(bh{p_BKvE@;XnLElzo{$(B8FhAczIZ<_jRgan&zYAE zFEiHs0i#%PCzhxZu8TaO)87)tF=qM$wVa95z>o>B%gUlJar(qxSPw}M`w(g`a2VEB zsC>UC-^A?9;{3hWvhP+jNY*oJUd!(MoD^^yCSeB3Qfd+c2xbAi6p|;7AqpG*BOX-R zV%Y9*A?ANHB+{eN5lM+LMiV0%5b&IqSjVHud{*463dBC>3f(-rDlCu$?&e0;eBaiO zDP7SiudioiFIXSF_FiDrbi`p`=mkLd*BfrTe}a-HnJ@UPEWvD=xqb#^?!xTA5M|}Q z#5pxM%!K{#(s2a^1(}(dPkQM!qq1%5n^*EI?#Wc;;Ww#WW+o<+H?p9zq;qKbpFmBU zwC;b!(EE5T?_LB1YZs#V=>`UVFU@`)DYe_+eK`WW56K)HqsB9v1Ggqco&5z-AV#)T z1ps*%XfE+xpqQuN%bk&A(kT)l_(&ziE|`p*^#%R=nQ$F@@H$vUfc24IZKU>W^sh!2 zk=ly1r)Mo#<}rd}$nYUwqRR9eadWQ^$vsiKUPN_JpWN=Hj%Qr_&kZYYKK(1>n+u096aX|AoB~}G%u%(nQ#vvZX+>N2OSBNNpmcYPP z0)2eA>W{A=>P+1As)lPGVRu3l&2-O%d-ePm`xvghsqAoXznM0?BTlSyyZGXMWaZIs z8uv|4<;J;p^W^nwH(3UU`&uvBpyq#siH#xL850c#lOS0~yX%<8We2!aug_*&_ zhSZy%n7+Bw?4 zyUoQ?o<8K_OA}U^@^ZdGl3b2Ctv^F_!?2BxEV0gPo7=2ZbcU3J;u>Zqy3hV>CoE>( zZC?ZZ3RKVy480$gIH=c)U*WUdd$lhaJala@Srq%gK~BwG*?S|7dX>dHiyBq>H$+7? z`cWF~F>V*}4gBKIpi?=P_7AQj@_-1qiw?^Lj~$$O{VUTrNed9<^SHk+#t zCOF^lM>;`lu{m_L@sDwpuY;y0FtxHof$Pnk!5-HD zjJ3Iyc<~dwfw;rYD0Y|=>k_B5XJllgyIXD*a5Df^Lyq^OQtY8-jmn?%R>Q3_X@X|3 z<=sPA&CNelIWOX`#y|m|5zd@UU2>CLaS*h?fW<%KOH)(4(st@78hYDIJHsd&p?u*+Zh;c4ZEnFMuUgVYs#m~mZJx*HJbnNqMhvpiqDQyCCR{SbS# zI-e|Z{*s7@af9D*!#gv~&TOcI{7U4XFj&o6_m{*CcQ-SF$_U zIP@ZE#VG0SX3@0j-46jp;eo0hI4`w_a)OpMS6`!=j^7|G@DP&>*gyA%#P0M9^&B}M z-*@>##&e@+#ipBleaGZ%*l-V?C?#=UTrh>j#l@+Lii%bUVgOOP7j||;eXdR_*p|H-n;0Xc6l@?2?QSHvP0C;%@6VaG^vJgyBgB1# zvog}Cb;cAgt=IxNGXn`}gtHltqF!f8ikjFN$LoV#4}QtzqBaSB<+W4*wSw6Vlg1*X zZdQkwX;fGbf1xGk(EalAgMQ|PXPt*vEnfD)*-=S2sR7ai=%U@QR`jSfGOdHjzP{}c zf0^&KF53(^msSOjc+9XNrSbk4c(~AsV&kS?zuwe5oR};%H~-5Xi4$*%!;odtBdfs1 zVE|kUTvArm22U5s6)T`k8;~k-wY-DGYHW=abP$P)iBOGn3MgH$YSX2Q&Wbnqk_oAa z)7!TDHe=G2#k&F_-Sj!ewP!M~>#$o0hjv zFOPq|7pJAO-Ien4@G_V}3yl{SpKI-WYFnY{x|oGmUgNOne>Xy^kX^xn!KR)=?E7b8 z;=Bwj3t$92hX$r?p8*ggzrXFC(NTz>4X9qL!E_mninxahd}l1tb#`+l!tvie0OdE1 zS@G$w&&t?!+*O&3cr=-udA_f2`+76WEzA)H+ul`6R2-vtxF3mdSX>@qsBt-_{=-;1M>jg zKHvHqF7o;gr(-$z#v6G-!ln?eVg2C=8gA*%$Zz#rg;i^`^#|xAG3iN&?xV$-gQpll zi@+%5()f=5E|ozxg7ub4Ux7g=Gw^Ybyo*p)voqiVZSWF{Br44`^}Ht!2+1@Pk#FkV zNp#pOK>&lAOtWdRx0Fvh=fN6FnKWQX-AYsfcI{-_N+HTXd6J!AAbB)|HPX>x2`IUo z3g`|Hi(9eAHaPd+N0PWO*Ub@(XYxx;VsPXnzshX2<10M%+X*0y16Cr&1C6<5Uetfv zO7>H^TS-pkT_xL)Qo874V>;$RY#2!6_@4x^dC93gDh;j<@Z+Awq$|=-4ZREYKAS}& zyS=%vVjIeo7rysgHe0&7tUwE4eI;8aDPK*Vnk5~|fJXMY#k7r(r2{BdpMAOg{igxR zB4U%%oOC`OR2TH8sQziJ|7D~|*N1<0=K^P|^Wf z7o#S_a-dVs{Tut#}p38GL4|alWA$t;~)QB@{gE&^RAfg zm*qP>+|-NKe(sX}U!g!tN$F&B3H);Jt)k)1|E*ZjEP5lvl=qNX0o%}|9vTZ-D?Qnr z0T8a<-`@{N342<^Kado;CpHSVU~cR$k>38b)GYaX@!M+@JQMg=3)SK&6H7d`R)IQ3 zkNn_x5CPT8b{lcq$x6Ffebs{i%7X?tcmJc*4s3O>&mo>IHjTh*V37MJvU(xZ6bR7=eBr1_*4^#NwaH=;XXYhd47>-ZhxT8FFV z$dPa;EK;(lP=CuqO&9mMitxueTsZeT8SbMUk=h97Mf?M3*gU zjiT`CuGgdz{V-?n6d+1rzM;U!4Ai?NE;%-D?;yf|v$H8LZ3698z;oh3o<=+=lVja5 zy0>yG->dm)N|yfqO+k*s2|ak%{`W7LPQT7Tp2>n^j$8~n*nPwENv=T2le^=7eUCbg?JNN?&{2aF- z8mgkQ@o#tDifiattA+WyhS{O-Gb&B4!kT~x6EDTjfM&9Fq=8s%!GG?@joESCI zx)0*zchy1s#BdB&?XZYWf)Nai?AHJqqU#5no21=E&hQ>XS|P&X=6#Hk{^;(B0Pw5; z*pEkcLLwpH8}W=>dueaB?$P^z#h0uJM~KQ_QK`f+V0ao)vMFwV# zDUkJ#IIvi~q)Y10)UJ_qK-JPwY zD8(s60!h#EJA@w;lcf6(?S{_&w8F;R zTcN>|;B0p$oNl(vMMUM2TGaiCV!|(7`u9^mY6=T?TLOYdb79ec{L3uUW-32uGLmnogc{Bw4;IoLoqUL|@#>bT$0GTuMHt*>A}Jt2{q_?*OJj>|A27d`uF_OtB?`;cm&|0YA2q-K47$3hwIP6s zpyzTFLdLVWn^78G0SQ0SfVgV^Qb4gCX@TKTi^XMi_z*ns?}z=#h>t7%k6*mg6mTn^ zUt!rbNBVATRi=hW1G(VnjZ@q2c-7sc_cHzatbYQEf)6tkC+y~ZUf9#glm5%_umwb!SbS(*TfZD| z+6*g&vz@8oFegBfl2o6Ef}!0bH-r!I(c|t{@qd1aqlXo0)zT_+tJgK#Mm3>dxqV^v zl;g$X;q8N>X)OFlg4p{-x~08sqpzz5pjLRdNLveh|eK*AFSVm z+HjBbW)=$ls5?L_20=$agTG2+HX|)QCH??m`A2OGE`i8Eh-(!tZsyxR>EXHGL6ngG zPzk|<2g~x@JdCpnd9BY*DdLK(*yKADlnIMR%!-0UNdlY3caLKV?ILvoSe4BvIbt%; z9xB?IFh6F)SL?FxHz@3J1ai|DPS!Pn9jO2dGpNWv3qH*&HSb!wgKv)H>VT>YDF#VuJbLbG5AYDg_|mms$n0 z2-yAuaY0%GRr&3fwl4RhQLE_2o2wcBd93AaJ}3z)WsJ!oCf2_k`d`LPw4=#o7CpQm zq$408Aa5a`goq%r`aGN}am|S(4Nd)GPYx&@JGjx7&dBR$$35%GvkB zKA)XzYQ)=e0gFPPQ|Y=jA6H)b&T2SIrqryXb%E4|1JEt$3Ct4_iECE*9J)1@S3mVD z*}tPHz8?o$AAoR0hAaCd=VWfvR)C`9+8iAnL+PO6YtIxJzHvhzPh?02aBC^(*D%S) znv5kfWi>mT?dWlAs361*DG95Nxe?W%w+VW&hB||X7<=gn5=|fu6hCeYDE`2}Kx_!w zVCDM>-nacBI4UC{0JtXb+o;o|lt6f(kG~`)>HsuMpt|u1=h#|;Bb2p-Q4;`ERTAb% zYcP`YuXiSj5lkbf$8j-0O~7)>&N*%Cp>#@n#K5rQx@hs7{6{WH1K`4c@m{ggVSyZ_wZZE0GGk~r{?ItOfguQVe!&c4&Iiu2X{qwVWN_tS6YI7d z`M3z4B6^U4jScC?JjcZ+P3#Q9Otl&P-eH!?#0$9VvLF~opYb2((3>pP)(jFoB>CKX zx<7ABInf$~r}14WROMrAH)S9(Dd}4fq%Fm4C3e6T@;F-Y59!AVSW%UWN;GQCIN<#S z;BS&ifC{UB_mSg`ye|k4OFD^yR%L_D>Nm0IO-y1M)Hzw_{)xU9FsSp>)34GAk(X@P z88EkrBEwuY&0o4C#i7h3$#h4fN2`6gnP6>;e6G)8HvsYY%BqHT_btb7W#HR2pvWVv z+@o6nLV`!K2MGAyz?BV7UFrS$j5)@x`qttSc)J!!y^a?bhm5t{attGiz+xO4mY|$& z^tm1qJQ>Y{#mcn-a>Q@i8VGQpVzxvsOUku8!wB9Q?!*r*MM^`E5&{_>&QN|)-`9`y zKCQZ$R)WdTqErud#oPX**7gRkb99nqf?5}Ik2SDgT8C=g_M@D7@AH?^^7P6 zN?$7Q-AE1&r4s*#Y0~{A_E0$rUEk?a^I-m3@RUDrQ5k4}y~}Ou@?pNwkP^)d!W-An z-~-^uA&QG3YhIX0c!5jqH+eS!owIN zuqCpq1|o|A$rtvvTqA_^hT)BWKr3H5hC>4;zm1FqX4Qp&9KLOb89F&jV zKru_R11<&C<%sSVJGDEe&<(7$#yVkPVNVYajMG1Vc7Fc@h(J(C$Y7`8TVvPlvUp>{ zQ39V%db@n=e;=M}@}*-_XENnrCX;<^zn4LlOd!#Ff#HFK%#*d zlZ`%9cJ~vS+W?Nc4UVzF>A(9_RDICLfr94Q@xho_PY!-`dJ06{E(2utXr78zu{Pjk z%1JGNxnU9R(D6v2AHD~;{n>h6M)lFqK$&<=e};(d28Q5823fnI81T@QI>t%9H}V z$H%bBcYk%OP5eAO^NlmTr-0%TK?fytGVP4m)W}m>R1d&B1tGl#kK?CD1XN=%d6g7# zAN@^rW(88NADo58zPg!jGW{_Q>Uv=!&fO+wYxVH#mxTtW8^eYkJsxORKu3k$_NX`Ab|wi`{PQ1Ro;=fi0u2d%7`QW<=Qhj`)SLM1!AH-TDG&!AOrIs# zL#l;8!Y3x{rV+Qbe!B+e2fKQEtbmaQq$XSd`IMZc5qtS=GQ*?%JGopl_ePM;D^L9& zzt^&Y@M?TI=PTsR=h(#KoCbu}n-K|I#()^jf?PE1`3Tq+W@kplM&HDJJP)_joP*a? zf#3diPPyLGETT6w4oZL6xbBFQu6A7hE?P-p=scVqEhdY(Ih)6I6%?M3mE+zN7s0?_VvQ?X99&ui_p{($(*5>MDH}6pN=7jGgd}pxVX7)Qx%HQ|n z8uUiS8%>vIB4KW`E&{%Eo*VR9Am0%379~bpWZmRqP(fXNgb0cxzO6$i6}jFizoUa~ zdaQrDr~f4omOCFIM_&9LY+VUOUb*7ACMj#y3W*IScaZBmjmc)2^H?_w!))ycBRlu2 zbI-2?hgM3fHLi!O}1FvP@H_^-WraN@P(eCO)$YY6HBt{PrUAnaRv+GZ?c5eC^ z5|UI+1o97zD%>AmCB^aCy7Lwj#n&?33}v?cCA}2&IASZFx?Tt%>H$U&0(nJ{>Rh0` zD@>ZmVMt=Hsgj0%e4|9moBkuw7%ua^%(w}4N&a>8x3Q1kpuQwmFWw83AX(RF1(9=J z`x?QoL-EkKyBYUPL+k^JBSeoRqs5|Rr+v}*}H1)c>6;*Ex;)W+QPBkzei6_tLcGjHZs2msacSD9_!8M$|dJ-)T?`_Kilw zZ7}Ucu#)~2Ydqi%D^dCXDr}s>kS<6zNk?Q)=rXK+%};y5xp!=n7&`+$jl`Fs$$3hl zh4D-H_qT=9q<+%)Dq)(Ce}*861LTF5)yI;YmYq?l4m%eGAUN&xwaNP3*sqRa39k|! z;@hzTClGBV|Gb*I#Pu4vD8 z8?X1ujct<2*fl4^X1z|za?N`B5XkE+M+3FmUVrJ)raOt*r{q*Jb}wV@5(o00()fP- zZ+Xhb<5xBjd@M{!r>!YNE*-ru*a<250~x+&FYNs+Zo;5)>7lEXjgTX^>wC<20wEnt z5;90>zl2oCW3pSwe0V3)YD9jz^&$gJFhWtD3$LzSF24t6rlK(L3d{mH$5zfUbupS!r>we zdK2HJSaRZYRJ9`gbxfz`Bbj@955MiCE?Gs`Py~XzhyN?PPK@PR8^Zgl6)lb~b!myY zZ@6Ufkx%Np&w0Qx6oWMiD&&a7(PlE&;+`qjq1z0SG9yzs$}lg$-Anmxz3S3)ok4EL z@x&rK$Y20GT3zSa9w)|DUX)FH=8JP{b+1b zkAG}l7(Z;hyqkIuD@3V)D|vZTM8gKGG)-Al&=z54Qtu}|L@dClKRnSW^2XTAhTLlH zN@!@r%cOd)n2T@Y%JDvxR^FxR_4xz2n>3FGD$!rTFR?pB?9vr+X|ex3le(L}ErYjx z{o&K(Z*^zV{2LbIaauBIrYK^=`Z8DEDl4hvdzw8}GrnG%_`pwD);GFD&!eer?xaaIdyrXR_z z*9AmdOotwpB5=KToh)CzFA@Sf>+bBKxah4sG5c@o8Q8HX{(h(4@w^7t!+ zrR9L-o7*dFsih3`U-JV~j@VJ6QA>n0BIR!u)w5n##~hOM3Y;rCc9q9eR@sta7cLLQTnt$u)-T4t4g@CqO*#3fgsuoEj$u}*8}{>TkQ5mNL?oEXgOI$+j;vp4>mP+b!XQV3%1tk;R3~aUM`f zdW=2{XOWHnVn|pJ!*m-IzfPmrTgoK2qZfU?gTHh!to3roI-&ZbRKW#K8_;?*pRDf- zdMF3z)_-~V=qvsLz`z3U$Nh~0SKgp~tZ9E1fXWTuQ?#o}ifd!bijQ}0h_){G4hmaw zk(e2nIy4LGLGb@f!OC^d{P*uw-#*-r11cDwMH zgJ;zt>6&M=mQwV>*BKfv796FD-SW61eZBM~`+pUm*Px57zfgL$ubFL0#`$5UQDY`b zIHc<-<1;-Kj&VN6ki#pq=mqaqn(^3Q0-U0pZ)ZvKOl}R3n*(<-g10LxzMTjrVNtHF zW)dGhjjt($RogV zIT8Sb=8L#Ob(-%b7I4PK=5}MUo9*(xqRPd%5G^YinSC51#}2YV(c@n>$hfE@+23ur zOtwQ!T%Z3eWK*@PmbYNFBGSM+INPr$-a?!hlD*%cz-3m1cpj=`uriI(#-cd0b%yL^a_!J75->-FN2CL#J`jljl`ia10( z8x4)HD=)GSoVf)aAE1OfAq+&v;BR_=)=q6SHaOHGIRv7z`|@d;PX(E18fic zpBP4ydx1;gXzdK>0mvduH+Z@6%Ph9PqKWz8?&-;;UvY7F#|0S8Utj_DFZjMRzs35A zTnqFW8FW`KziRp>*KY*SB{&*bQ5pci=hACTwF5iAtHbWH+aBBhSK|auZ4Afotc<^M9;N#U-v_57f#oMV z(8+U@t6=&&Qj4uYx72JiqymRIj+G&+9EFHhv^h^nI19V=3ZnSJJ0F}TwwtT>0P`2v z+KOp{JdMCYV%h`A6seGkTyP)Y^aC;85CF6s|L|gjOyMK8V+*f*KwcK8rNFDPn=CN} zj87r)`E0Q<)Y#a#3K3W9p!E}EyaCvy0LQu3SpG;r)IoYxngAR&r7ZwFVXgP;Plv@; zkmhuDbL-0bL>v@wCy){fEC(!fbZoU1Gq@Ri=@tD;)7@;KMo)GnGQKAKMJOmDLMP&? z6f*X}K-I_FyB$sQhi2%VXiq&il;_<+<`8^ueSHF?4#4rx9OYE7po{6Nan+$A_7Yo#`X;eDIG75zzIq07#sp>L5Ex9r5Wl_(#7qK z*ME;?1|Nw4=KvPWAL)PRCYKcYO2pZ)^Y;nl5wAr^czC$(C!iyN^Xs@VPw#8}7_yi_ zf0FGCvhct$$G|0R#+Z{G3Snd5U-^?RW>7&`CjhJ+Ho)8O9Auw<<`27C%n1VFeZWc? z;xkl3q=ax;sF(A_Bf>!bd5x0p@h}i|tooDa^@9ydK{S>eJpVX?gA+Ooj?W$lNdQ&) z<63_xob4q2|4{W7Kvix3AE-gAG)Rb~ltDLgq`O5y=`QI8L6L4jIu6|k3W9_nNT*6j zN=bJg>hQkY-~Y|LH^-SfbG`TMefC;=t?&0!Yo4lfE0@`IeA%_fjdjW z3(Qz8u5a#&VekXX*Gph2@rUUAE!>gc8A`pt6;ws-oG)qKCmZ=Pgz=i~2jSD40Gfa0 zF<@J!MV}FdV*XLAcespOzklRSAIZAFJ>7DV$^yz&9lCy-m07AO_0#>I&n_AK^byd%JOCg~n8BGbzYWc@;c7UO)2P-e2(p;UN}iNFhc`Fz=To7Tc+V&s zAZHJ*K*6IIRd)Uvgy#pc5mpD7 z#7<331uolv@=Lok5I>VA_#-Ii3i78AP3RDfIDGpp22 z!LPm@8n!HRVO{TE(lI_fX=8@yPl zY1c-)FeKa~Ime26lmRYYFq~@UDFjMjjz`i0tCUM-c_|^uu%yc0gDboYL0{b?A!h_2 zKj-aPmFon)fV(QNZ;zE64i65b-#rBo)Ox7!t)#`6TJ}{yS4#E2)me_iEv;{CT)xQ% zuquV=j(!c!5*Lga1`Eo!B}f3ctx*6gn(^8jODghAP%v>wQFX$P?0434TpxlmMk^g| zdl{w^KsPd}PXWB<)L8|6fkd&>YzwB1jm6^$$zm3u$bJ|sDxJ(&?+PUr+{Y?<_gZ~KD3+s`WC_ci+7#n^}%X9p^g)*Q7~Q( z8KRy#ehif$H4(aWPuwp70g0g%L9Ie(gpt6N!+b06>lk4<5~c~+tIGsnkMQ*LbXyxY z>tEM#x10VQxc#qdo-K~W)pFXdH%YxfHC@B5(fg?19@?q%sv^xOT11ctXw863^x56- zLhYFB5ED(N2{v1u*L^uX!WJBbAc*<`rw=C|^+d35QN7u!H*7wGwQe5Yw-8q)s=2wj zj&{2T7PY3R&cdIhfz8<3osu26Xnq)S@(n&#uD`X;va*ZdBo%~x^taV*ss=T~L82lT zwBQ;ndwwviV%934#*?{N{36U8p z1s$E)gM*ixc2tdwjkcg7*%TB~Zk>Qli~R^dtDVEC4$K?3=e>?HUMno%mOb~VH{TE* zL9%9F>Z0nAur~}<7)in+9ZieCOTP8M_MT#b(nQ}ph)5-Je;RbJ?e?a#8dCV|--@s*kg=*g zDUyDTCPsy$S@MQVJ=W%gY2C@|AFBWT`|aq8y?P=yY1Q9`^EWn3JZC7-WDowB^`hw1 z0Z-AsoHA1E9I76P_v~P+0ud6?Lof81P)bLj+&5T$yVR8M;*r(12hn$fVv(H1Pjnh2 ze+qeT&T;H&*1OT%w8ti(EcS8#kq+efob30&U1yN5&K;(Ee@zl}1d?8wVg1+IL=8u` zx|^GuAgv5*Y^L8CFge-MP((onM>1iNFuWvL)NHoCc%y!5I)8rjRdAGc^chsJ3bVTA zIP7W$u19yB)`E*0a1Sq=L5LBN@+i4Aj2n5tmK%*shbg=f^-TerBKZZ?r z%~NuO@0q>;TB6BqBgK*^zae{+P0D&eRkul!lI^Ll*U38OBSM>-;Vwb9>k>cQAPw7o zRlPMDXmgWYDTeN9qoc>MRbPth*ZE~ZanEqNz#7<4p8dtA<7b%=fFm(m?y1=_(%b z^zuF`Htfc$D1%n6kJ1K*r`BxJwfdk?X8#I*{qlQaE9|z;6sG!WVw~Qd?Ob)Vu>d3U zvG>tS9Ua*})QG>h?lFIn2KiB1-^e{IB?*x^p6Jea6%9Ugbbxs2{Le6mO zAIoAv1&3w^ZG-QR{QSO@98`eoW3mO7Y*;{y@r=Vr*&IIKU814TgO!Z)&(o>}pmKot z{SkUYVjrFbJPoH{%;uFri{kw=kvaKSf|OW(!2oas+0f4qTY(pfar`p~S1-MU@4GJx zqu4M@S$X7u8|qwLDYnvJ(4qS_G(NP$fxAav~xuu;Ls! z5q!3zgj2-XzAy?lau4aDBKra0XgT6uwEQBFwj6ISyD1Niy z!B$)Tx`s4NgP@aT`zSgPrq)_YgS_lpLqmYv-enc0ZUT>H^Z}!k%=p&~c-KAm7IDwH zT;>pdXx*VLV0UVr7KpQ(+WvQ=bw0;VfZ+p39$|zn;g=~BNd?Ivga?&>NV5GdX*&2Z zElI5V@t1Y}`Q!;}}$euB0->Pm4g~SggrIj+BdU zUH^o>ykpZgV3udDzbZVvw3~utA;cQ;pmG{nrDQrzNQl%y9p(5ew4`JWL@N@n$xe~J zT7Jemp&3DzhzsYoh5iSnDol7*+hHrEfm2^VrD_m|0%7q$gVak`eGmoItQsdLM3^$ME|V^-vgOpyOVE2<-9`qU7ul z1AG$1DM>D}5cYt|zrpw1>zE!2dZ2FA*;Roe)>1t08W@N!!2GY~2YEozX%vj}+SZnR zr>Y=I`Tvf9J}-!wA*tfn7hl^6o5d({*c|)>iEyh{U;}o(sPXx<;aC z&#n(_B*h%8V)Tz$OJ0y)BcN6#P)vp|`yLqP^$D6_A<%%+hmrQGUY4N@Y3Kwl6Pdh| z>?)O)_<5`5GYbp%tScZE=7AXA`%rj;pzRB(P$MHGuQKv{$dODLlX( zxv%aVR&Yths)t^~#G7%D{J;K)fqS8jY*k6Y4(Esei_DwXZ*WSG5Jcg7eMxhJs6`g3 z_b><5-w{Zcf(L$U?mY8OzKuFe7BCvF88eceD7`nJ<#!K3)^&)RskW2x>9`%`7j-1gb}J*B0d)W=$$_UH`*6Htdq#nBy1Fq)1El7hHXR% zE0^>63WER9uAy&?1Q1Gukg#?Wuz$++szCfg3KkV}2Q!{V4x;-AC2;8Z0*k796NlsTGFDah zSyYOL67;hPocj?J5^{kLJx}w=vh24qnOhUtMgl*udH&yjG)nl=F3PRHiCjWXU*QcB zIbEl9Lycsv=K~dE;}K4|_+x(l8v8FQ4Y#3?8f3mTKszDoyt_Sp@)S}k;qSJr51RbK z(5>L`T=ajYukZNp-|+47chSh&M&t8F5(|5Z2y!7ZlEes2OyT*!`$vxKbb`cL8rnXa zyHi@!N3^28Li9p3q2m%QCPZ^X$>Hw%Sd{@}@$mwiqI&hmGg0RS)2QI0bj{zNqthE3 zt(HDnlwTvulS?(E_eQo}7d7@iz8&&pJI6FT!lZf;)4?M--ie^b%=%u=_KU+|bvOCk8lRXk zgYvV4i2Ldr2h`82yG0%D{oSto99J1LW%zOQ1NKa@u2 zROvF?t8jX1moSMVX&z$De`KFl{7i%Fb?+P|j{^nm;lK4AJyw5%>0&k|0Nt=t@|3q7 z^}M#9qcIdg?)gf7kU6@UUbJh+=qx9QKq6!ZqUA^=C1}xI#5M2GQp1@DqB%3Pi^nuG zUvnKku}1`IaDKvKNK<>}ZTig6l8uUX;)_}r{IH}L+1QfK6v9$DL0?PiIz17;<>4Gf z)|n@qOe;moRp{JeYC%`i;7O{Ur>s$%(+tO(uSy(~kmLa=ge>BI*$79CeHID7@hUf5 zVjKNjl%}1+n}5l5^Hr@ zAGOr8QDnwn5tjBKGZr1DAH=u6%Wa>j+>HMJet9g7;=%~iPC-L78l!+nQ{DUSSQDi@ zs=j`7BcMf}@Lug33r`&ETg!Xz%2{GM9JG~wGdC@(7IJxv9b%}ZiF~4_r&sa+?*&{| z4L}FbNDHn{l~AvipWzz16A?c+?xfen3vMsv6y=M1tzxbE0JDfaA%6Q=%Rqm=Jhskb zDjw?){$p3au$v9h!yF;w4LWh3cJAQP>tmjW!nZQCK0e27!8jdja!zm94LV6h@6Rl< z>1arNvN)w0vPl?_#1E8+42o2%DZlf>X{gxD`Mt?NTm!HB+03&CH^iGBAs@e)yl8XP z{rC5PgoVI9QEztq-=A~b9u?GsjFZ{?`kJy(JfAl)gj;XnYS}QX8E7VZv?6`jbu~IR zIRXt46N6uQJg2#popBAU*cI{A#Ee2Cc`CG1^g@j@Ptueg+^YX29kktkhj>iQtQ&uj zc{wN%<=`sy@pKxqIO?D0e6-JHJ*g-Yb-!~oHb3c#)TwADgySalayY$^$wh*Y94L3EUt2vCMLg z@y56K`js~}LszeSe+}`*m$}COm#h1~#M>)Zx=|jLV-NC3r#Y4yrnly6{-ym{bG`B% z@#zBPx{k2BaJ;gP(8t>T688B~ce}0_(N$@6bnA{THrnrhUxlW{7So@I3VNrnYbBiS zGUXwLY!m7rIX%o|)FH&YQZ@3MgSXL?@*^qeH2K@BiE>^|?2MA%O%W(k3i#wOP#O7q z)Y8jqwbt1Je|;LHTGxx7N|0;l+x2HmiKL{a;z;bR&UYL+*f`#iCD&u62vCM3kDuZh zOAuXV{&NlY1v}~j9*~e-8YK&l=_l4| z5yj{*hRD(t(?0{4+o~o*P(HZb2D9wuXa48#58a8ylA!6WX`Z;bgT`^8xUo?1s(Htr zIjQX8(}>O(+6!13`pIIShHN|JL`Q~(I=VUKj-MSn9}9Q6xKu51=;eV!#a-(dKpyZc zQ1h4%CG@4{D!!;nQgm=2OmQ~65M!jNELCR}g51)?EJ>`sWS;Q%497Bzo3F%&Jr91F z$cjEja`T!GWO%NRs%v>Ibdxz%oKhVr^ely=&sokj(PR>eQt7YRnq-N}RYi(3{1x*Z(*0(h{!h2oE8P@KF?s28 zZHjVVZrtXGL)iblHYXdML1%ZDHU7`eBL-e#WXyvy`T1hL*eZ@>5*For6)Dr3j-I-^ z$KGordSQ@uCbL^qP}2~o8iTh>Zn{T)+cR8L>x=alkbcW8h`$S#%NNxEVJ{eG%XnVI zfY}V7psQtaa!R4$s!{@Ku`H_JZo2kr=lp$!pcL%P?|a!LK;_8IpNH@DNOep^Fh5zJ z69XsTf@2{PTDKhDdk4wl<@zaAXARSSzt8r)bAo{JP=(gUf_v(l1#RWSN+Hrblsxn1 zDZ$-1Q#|sgAnv8F+=H|>RxoDqCu7`xepGWDT98x@npWR1ANp8;U5SqlZgB<|bf}85 zS&tSvI)#ODdLQb>g7NHm*<%k>0BUvErpq2=mDc0VE9{Dab1Y&32iI3)`>U*nPagYb zSII=CRJrXf(!^%yi*ZuQCwyN{@+ekL=ht&x`QH5FA8;12!|xwGtahgBGKeF8Xum^p z0$SVW3>{JFRi!3dHbchVvEk%nS-k+gv+I6!@Q~si1_&=5>}sHL%^8;OvI1tP&T$X_ zJut5tekT#h4RhlMsbNSQu|{-@YU}NX)z3x9hcW~=o#yrLrB?a8we#@7f_@j=gykkIXlP%Q?ZiLOML*-*?Y2jEDM*o@ZK)0wKQ2BWgobK1v61W7ClB(Y1( z`(DK86gkCX-q;AbMSF0Pmwx-v=irqBl~nmyVa3EUreF_nLxKL>*zA?0&q6U@&+}4d zjasMp;~V5V!rs^gGLfksL|Vjs<9Itn6J4+TvO@2ZwCDb49H`Z9 zsGA(JJbN|*V2}6eN*r9`S=odr%oDe%Ac8~dk@Z@oCIDEf)srhv7d^7X&b z@$ub_8@C_1I64XegL7fW{4ivZC6X!y7BSD0fnH!R5sbdXv~MvYxUNy6`Y{~E!jXXN zD$`Uq$pj;}b>%cljluSI26QExP--?x{((hnMb6)Y?=i{Y0S#0u_RRF$NI`~ z3DXMx{{A3D|6<-pCD=7SP9@cu!gv3}&kCa-;Cy@{4N7#NwTEimxAZM7mzPeqPL62l z9#!hqA5pC&nMHEba#K7r6Qqa~Y|npx5#_Kuq!Y_18@0&cc;g$l@I|YT3kL_s0}aqR zw;l3>2MksbUc1R8zZ#g$m%+8*@Xut*gCq;)hkg$cRzp9}a?jn<0t^{#l93;-M-}Nt zN$!veA^TM&ghVm1sYOW6rljoErlK z!(&dg$pqUZ-k;q05>%#Bk3O1Xd1x3@QgUCv8b~-&Fsna#dkvr`8etb!w;W(E)lZbq zG+R`;?+B&^eCD#IP+k-^FH~`3s=o;Faidv0V{$V-VU&FH^&V!V> zZRT<2UHBa_diq#)Nt&B~$rFZs3HakEBzHJgdW(BLnu^c-^f29Nh2X`>1K(`SkEF^e zqS7B3iJsrT6{-8p?H$ug&sfwVXV5Qf>)qtvR0d4pgd5nw0oRT8H97H*unY1{*X)1q zhRZ`J3xF_=AFnC_7ub9dWYh&C3eeV^{mI{>Ei^qk&f*_`AtnvaZ&tHMZO>L4Oxd=X zn2l~QM(xeq)Og|MyvMo3a&04~(&tz~p6XyVb+YNk&6}oF!AR6mfljF=UtXo$uQ$H8R=U>D;z(7bKl`6KGO?z7=EIe=Npr7qzRh6GZX;*;-BMx1qp~9Nh0P3%2B&`x z6^VlR*YLg7Wihzp#7G|L*R}js%=dU4KxbwY_pN@kM(Ks`)n+{ZJ)A^cAZY>F?C%CH z!s;JL=9hawH=j%|HE z^alsZX6>o}a*X92IZMp9e+a?gV9<920ENf2^ZUh(&0ipENI{GKI?Mgv4^P$~lj-Rb z>=Dm5XIH%AAg`owZ?~47juWH)y{Fg>tZL-IE9K)YZCA@@KIHb;yC%jYPl=69x@Nb$ zhdp8perVj!w!U-JQ9jc&=mY^9{oJsxiGoF<^k?bdo$D7mO$LUUkyyg!rRv?C*4rU@ zL>-GAh1Yd6PyzbicwY<1%gLb--WFHM%?|k-p4E(Py4Baay4H7Z>eio6Og$_$9@N7M zOV14rxTXCDhqaG$dz=euc;9K!2ka$hMoE+dTb{HTr++;y2xZKDoi}Tj2m@CqYPM4C z8$%a?3kz%;d^v?|{qGYNUYL@YJ0NQLn{i3o(a25$W7{w`&lfct4A5>7XBKL3gWumF-0@gxWMOVdWUIDmal?Jm{C9qq1;_Gl zo5{VWeW&^rT-27v3SD=~0tOpOJ~&hdIA08;-;_qOqz@&Xj^!63fh zY~!;Y`MUP8ehXAM-ijc>_|_DFt&q789_e87Bx4t(TQMxF^0C)dXz~UAdi;>7c+&X5 z2pATK{(MmRc~k)vQ-?e4vlrR-Faj0lP|~GY-B;U-P3!$prccCHssHiOW1kmDcOAtN z^1$CKc&MN025OlmJ?iw>%K!%#OfEKF{77iPemUVyL`S-8_f>wrIa}?~f)Ng7vlzdU z`iQE*wUL8P#^?&4y-|}p(bx0rDoT`7C6L}|A?oH`c?E^+Y{sq6qGeAF8KjiOnW;KsA&L@FKKq%tk8hDde4WR175VPC)r25jdN?f1)He;{FsZw8E3O1=88W&S-rzWC@0 z*bi*g?1w^Zj|9dtRZ>>r*(N;D0mC+ta)mY|?RlycwJPf{YozlwkB?OtUEgg*auO9? zy0Fl3!zXd5I1~v%c6ykMaq*F0Dow$F!Vo*P*E6+|Jg9s~yGwv9xK+#At#n|tOQtO; z;A_pbVLtTMMMn^{~?nUy8xj~o2?pv(h#qogOUGMcz zu#>B84dI>3pZ)hi#ydQ+Q2lZ`7W&*p3f2Bje~+rj)>$@jJz_V5EQgLNdc zLl>Am?g~15@97EGLm2q%V0!ZW)84)wE1!eA*a7%6F5f4do|1jQ%!uC2;N9wZDEBuerHPs>?E9K;zET=2Qhq zR%&w5YUYGIhJ$ywwt*>7{)%6^Fg7uv85Wdrwtk)Ab%w6Fa-r?=3>VB67Nk$?u&^?r z9fd+d#bCl~`uK)VPcBEymvx6lI5+lN0MF~gF$<Ux58~(E*lhbG4yj{4H5qay= zox5pH`QTKjw2G=CRxXUl*UL@QoDcivojY3p_3OXIMeCM6Njs8Hk6*aCtc`3m4OEd} zxYZHr2w070qe%R|1)y&x5MaI>d-Bv=!9z86g-N?I63 z;9ny3LY?gmuGlxGbBdjh!C{f z{>v}@KrwG3W2d{mxnGlvihD9mF`^{DKwm0RPO%YGEA8~Vs7(PMjZN$GM94##u$Y)z z>=P@=P#YOsmm70V^(kKA^YTa7*F(nY!j4^=^&_I!Z#+-r>RPFe$#gyr_B}nYXpf{m z@A@-#ND{-2wHbA|+I_!G{?_Wn-*1ZEmAfo&6|hU!&UZR!pV}6L48d&s;%5T%&$Dth zUYXq7L9hq%iF4w@Ss$67c=_-j{QbYZhm*c^*?&`RdHjkJ&Z&{lW52M8#+0j9S(r76 ztMh5bvSNSM-d@bT_FR{+p>tUDT>g97HVYMnCQD0y|Hm_S+@X!3Qqs?gp+b;h6<_9- zLYEW+r5fHBB`D&&{x@TGktYY6#Z@a_=+>|dr_$?O)R9LoK8?nAo!5`>xMc{WJL-?$ zTSH6}g%9F9AQ!#DFMEJww$ z5yHd6d&HIXFf~zs07(&SuboSN8bq*JG9dc>rh!npm?x9TP_cb^KUpU&;;BD#QiDAg z!@x{(G(o%ZKpcp)JeC2M8w+Wu>FF0RMiR=_~gO>G7UOG19 zA)B8u*QT}K9Ba4mjG;gVWe$7f-M_R&b5Tl8)2X{G40m`VsSv&m%ui`y!7s=ZTJv&Y z3x(c{|8?b;FJ_8OZpvYJ;o&X`P9SiswDc#^aCu6CvG={X`K_@DuWXI&|4l!tp(d7V zxri?aw+U;kl6Rs*XT;NiGJbX19#54SRFyLN6ix4t5tWV{pu+T~LJ&KDMIC0Iv`{xl zf+*I;pYr?Wd6r}_ahBDuY)t9?B>L$z7Mu;%z42njsbZ14A4TpOvB`=%$(>|{80#f4 z<1AuOBUWhD`kAqISaw+Uqz|V(1Q3wgVRe!sxeF$K8P_^qyBwV3=H;AdAP!=GFP5ScdwKcp-h6ovvzy`mn>~0 z$lKBs80J*xRLz4?ad?bui+w4riz2Rr`NO^+f;80c)dU{ibPISW;^^Uh(KtmRdGwLy zP`XS&jFc=Z>E=sX?I^@6oh9>^cvQ7X&UTScBp*|?j3zu0cg}e5`dNb;`*>_s(Yjv? z25Irv!Y6Rq9N+U3Tb#bm&aZW`IDTH27hViRk({pJ&^W4*X-da--#K3K{+J zX|g=KL-9g`)4VDd(7xO!gYO8$v;kd!NG+mz$PzA5DHz1qd-KO8_P6atmm)`F43g9- zPx~D4X=q-_yi{!BPMIE|$225k&!W#ydr%}hhA;pH*0N_sy9B>`adqic4+q;PBPv zzjkhJZns}#>oVtgS+(KCi!4Owe;+euSB8eLVLpG&<#S6fZv)YOLfD z>uK~+Rv5Z;O=0C+h}Q{V+xz%Eol3fY?lyPZ_Ip27}7w=H})={)dj z5z{Zb8n*cVK7)u+E&oGctAJ$j`(Bi|a*)M`bsTu@6|>Q@^s) zTxEZeaeRy|EL0DUs`PTjcm9$;7>+hYJQy}iKe;{o?+5l`+{87?b}6&&Rl_1HR+X2} znjk(H-K<;t=)Lu=22w~`r*zgyF0bL!iVWLTso0nlUERF%tN8lDol#=-!YS`Vdwg7j z2Giu6xcf3+46~ePvh%37>>H00sE$@9%Ju3Gd-t~xKgZfO z{uQlWKnY=C3Qh^5ndJV3ieflrh+UjH=OEtihmygQdu&NEw(v9z zq@`NWHWIRN=dq45!^C^q6@qyCh%dIgXZSSBKz7RFb>lM{-D!#bJPXrJL=0iA*=bn3 zjp#QY8uNivBZ0xOHkxLMVmFyKCZ@Yxg2`Gcxy1k=aSRpbwbvwW|CA&sxq1-CZ--a* z2H_|>Z{ie!_(ii^W8l}wcP>x(lTAcK#9zZ5niiWiA}MG{Tm9{}qGHP-`lXx`5tj^{fY@#) zBb3oZmm?_(^9e7$P77K6uIBq-a=x%i8)@EFuDYMP-REbo|J`2hh@eSKb84CTQR2Mx z2vWv`D3F#e@2~0o^2dyVr*Ho2p{0(4m2zlU7$v`tR4~4uax(YEOmk~ahsK=SR(Qx{ zd#pkIq2F-Hk!x!8hJojSRyEqw^EB%jiX=`K0$8|qOSD@|f@Ej1e0X!yK( z4g6e9-}v^WEqqVff@2>Z%?r?o*oj`eQ01U)x19XfrO8w2RG?flkK}TkGkr$U_Y=wg zaW}8TW&Ayj%3i*@YW&p#B`OZ>+E(R<-)U4QS64(U1y3`hCv2@h{;of9QE8|Px`E$n z8!=UVvSnqGuX>lV(ltKCU5E3B89KtX`ENuKZ@b`;)a3X=MqK-?yeYov9+V#O9RI*) zYJpe3Lt?(5(?zOhsn)hx#FK=anQxUt5YPbbq$t0?^O?!PJbX{{rM+ixe?xJxMh6zvUvUbz3-=X$4BO(nAJ=9qc};^OQ^ zyN|zAwMXQ{C!Uj|=?lYl9P+$I4g$Lf6U=h$JoHr`#xD<06ZTE1S%_PdKf|f^OFtLA z!w>Z_`|WX+U$W8~YeK^RYiyg~qsw}6-uGKQRlN>Rm(sk9-!9otw#600J`cmD%?PW8 z!p;8Yd;{+c4cle;`z3wWnJ=G&2Xv$IPQNAlxdMuMvlOS=`9N9Dh(Sh0#SU*LgiaE( zcOW4(-kF^zIIRFcXNNk=U&KF4S8GH3bEsTo@_YvE!wP6{D;h%fbAk`w`V-f$=$#J zks|8h;ek3i0ZQ-lDYKxU2keUAYG=`cY;1oGd^pwb)2B~>kVZyFTiiDW*D;jB(FWLf zF?TNnxHa%ek&{~BksOU|f}-kSTKtx(o2IP6BL(4dk;$6p7Akwc-R}EI1jXXZa5JNx z*5xp?JRL_0SKDiSaGg5duWya|#K+pZ){ww1*J7b-pAVa#LE$WTTlA8{=%mx+BVGDL;aVw+|2<6LFnsQy+%vzcYZ)uG5XXh1RJ|Xi$hgfYJQPUM<{BJ%klVO zB;rTOLa6lPkK)wJmHjQ>E?1mk2Dn_S_Pyx)pZO9zxt7Q3it3KXp!w7GT7Hs#-#Pq$i;N^jx9B%#XI@2K;xuCHG(GfQpQYnAFQ zFwH*32rlG)f+ksWoE)lrZvSPO|8>w~`KJ+zrzEjMITjplU0hhVpSv%XAF}L+dmGu` ze_lpErJyHj)WQT;zKK6An~< z!Yj(Qci6_{)3XLdwLP3TCU*7%>(ov(+{tBxU zwdbmwpIv*JDW~(kyu2x;Kuoi8@nOWhHmhCsOBy6lli!N}$*y5t9B=*50OwyorKg8IsrZz`@Ky2W=Y*(5*9A5qW^UKwCiR-#ghPqKWPq_?l^)G8!^To8N>kd?l z@D13Bs1)(F33~M?Xpy|_P{+hyJ~76Gl1Nk3ZQN~bByyF-^nH#RP$J4oa-8}gZajcm znJ_(@BLizrwW^vD+-S|a$pA23mc^GC$PSBh2_oug(Y8kNJB7K{l0q*v^Nf_VArZsv z={r(cL#ua#8<%-Swn|I=wpS(tDm_D)SBai}aH(BR+93_Jo~{lIJc*-g;f2PK3U?x5 zF8;axEHnS_Yz1x7*+K#_)SDy{lZG(&djMbVu#nJQc78pAxqdy7v{&`_QUqSf_3adf z76tYQ}y#^x$3qcR|12grUR&uovkjq)Zb$E435ny-sTu; zIAWs~=j6d6xSN)c5F{E8$fuKqSeGi@=~g$x>6R(16JY95&xsCv2ikNcRwHNJ;%y}t z7hZ#vx;FHeJ*1Q&7NkdWSuGq|x-`tg93X zMvQ=!l!a(ExAm>90Zi%Rm6HfTJ?q>}j{xaAwH*>styr{Ev_)iVm;XB)+zA;9^$_r5 zwE%;NHWh~_{yQK%mZv(8u1u>^8)ClIAdU}E1#=6y2_?|z14oDX@IB_6oukg}#K8Q~)#1a4RTqr~NL@!I5SiABEC{l&AFc z^jith<>mGuQNCp!4UO8*wr{|Cxb!c@c3&0CPj|! z9@R+v@F7({Q8N?wY~4LMEV{%0Y07T(a;03$-+Qc`lVRhn#u8fzbwIH9MN z*G}7Qwxd#9e|b4oRr{erqSKdOiY3pAfM=GnyO}Tm&Q73^?Kz)cJ_s<%Mn68Jh`#|8 zuoiP*T6{em*=aS>0~j}GrDEz5< z3E=S<{^1P@Kx8S5YY7Sp(rJx9yuwj!|Lx!z*sD6v0hz^nl3K{F@*?Wla0pb1+^v0m zeTK&sOSQ1aV^zY|N(jfuXqPcc`oN)&`{6%7zKbn*8Q|}K&6gU`^Lwwiz$MBjUMLER zR@vhS@aS4HKRL5PZO=l0A6AbAl!QQ~b0=M$gAKf!Jsw9cy*(0s!%_LYOuOc9!DqgI zq4DhwBf)6_%)JqPVlx=X--aCJS85i39|FE$IMRutm<)ahU4N^}=+R&AXq3V+o3ezq zHidj6yk&ZyO+(GU{p^-OzSj1$M(WV&;7T7;rkKShCCTO8jA*E+P~)M5k|`eYzWx{< z$Hb4p3AK_3u*k?on&`LcB&UFZ@De+iq=1=lx|nLe3Sqj$#MwGDC%qoR+i6{4j}z+$ z(xNNJ>JO|YZtxg~g}bCkzxAoCAA}%i9^`sQ8STemI^8O{G-=C{D1X-y}q z_g}=tD#`c9F;9w;-Y@8<_uGOnx)CPTHcEA@hhrL1JUWhnD+yI8N?7<8uSUuZ%9HQz zu!P0Um0|S+%Xxr9<0i8d3_+pQKCJcI1<%>0=rN!X?GB6WY$12KuU&m#Y0CeD_K3QV zQ=%A)JdZakn+n2kWc?}47i$;>IJ_s9jp6}q3R8@{At&zT7+h7VmLL57<~MzcTG7Uy zOaw(1o|%pdiSg@ab7Sj*;d?-l;09W$r+7;CFoykn=KM~2rvRL}aH#5-;jxsAj7CPk zT0Hzf2`49RX?LW@9Ii2w)}N<}a&mH+LajGA8om>Y+u41TZsap>)$2rryw@IopR0)b zraZaE~ntF7z`-BOzVfm z;Cpt+;jj0Sm8^I?#vG)rFEbJM)-+mp_udts57qm^5l}@f#mU#_R3UJSmd8Azc{l5E z3*;Ud6<&k)XpBzA&I8u!(UKC}Vf6=L+_|O8x9*CLN-eUZP0hDe&VMU%sCOXl-&f=z zX+?s*ge}OX0&?+s|5{1jgE@lJpm9mMt#8tQR!mePY(VV z+=_=1impc{Ksdu|wFo2^zbWNOil>qNB{|w6^*rqDZNu+OPNVcN>lm2tuN5*}_V1&E zj-2S2n&_s~`akIFxEEfI2dKDxg6gQxaSw3gv@9wf72>O^^?g7yb!zB=1AMt&!fXm; zAC~^f#{e9-phkCRwBibs461$1d|HfbcA`bCdGhZpMb@vr^i$APiA7pA~%9db}z*xL^Rn7fuF$ozDV$)zz-mNXD75{P4cTSY&-J!kP$8N<-s*x=~X}_0n7)^EMn&=_zrv z{y3A_UdON4$mH;n6$U!L>XQ#xavec3NWJEd3_`s3~I*Vn^y16I=A|8_09?YqO9Y&%lPg)5?1Sehu(2q~Ji*N&!0>mEC- z)TK3CQ}i@iTFcWwuxXvLHA`~%We9p**$ZggX{Y9|;V0kj??9%Z;>P^nO}kCJ;`dh@ z`^gkQCVWlh>eZ`FXS+9sg9>@}XM<_SaT2-!kb~pcHsH{Un03EDdhVZSlfQ{q9b{HM zh%(bOtGFMxC6S~{`O=lEV$0y_rkg6__-o&-Y^kz3Hg!gC*}F#Xu9Z(=q-iTx5cQ^lx(~cH9z&f( z=O>4UJBPxrPpFE=N0Up8!YwBwVU9ja;jy9QiFClVF|_jW=%KDtMixD)>I zz%K3W8!YMq%a|Shw9we!F%}1{mMkXo5&ORi+g5A+QxR~O!^|q#hs5%iHC8wN^g17^ zB9hx07!|Byhg;M--rSD!jnouQ&8A*Lka@)`Yk$;(K|IMm%|4^!oU&;#3{&c43G zy@SJjhXTs*Ize7u)8o&X|Ni9`X|fz`4*wV!c>b+9es5BT8mrO*g6|r9F2Swu;=6-4zF2eZ9WNPdlbxMiOERdk#GUWB>vsmVjm38rlyvwznaI066hnGI zxf*H2-%q=6fg?ZQ=;XaSV=?u@Rm&0xxH?t zWHt986>8%`EH>Pi$+V9f5C#8HanBy6aPnBK6SjZXMtZ(!wr@iB{=qu%TPpE#up1{Q zZY^@WJ7*%0%zP30D)vWk^uMEC%_knAS4S@%hgM_tr1=kyz>*)A@!eP`qELklyGCH{?I>CwNt3&|-0haz| zCxd_;cod}ZSY&u%M@CMh8f0?JtAg=x*5yY~Uj)G&rJ241pgsG027w?%_AV`w{ad)b zYxkO+hlTgSE~Ei@tZmd3D5Z0`rcBMy|M)u7!(nrJj2$BeM!#an$c6)8m*AKd0sS(f zS924K(WTJsajcC0g&_IZ03(# zvakNPqSZ4vj1>-WO`G0^YO>+AEGhf?{MtZKL85AwzGO6E=D!alGKtocUqg3UOrku! zWSIYC_D9slI{pu%61s(ZYl%C<7T;=;o)36{vV0t@M7(RNy0f7atGOu)GE=dNaNM;9L9JbZQ*X{n;;=G)b-3 z3kQ{81>N`Pf$Uqx`|tw!ob7a!e>*K#orP51IEaPFkNrI2Vz;L}Ex~hhDC;J4VpO9w_OxSY zs0>>1B|5OyTBfIwO_Dn-vod1mgEtzWzp%Ajy8rs&@!$wXNoHndOieGaE<1;wkNp^m zo4*3#(4MM0C7Pr|Rh5>CV^tWKk!jXE$?Jn*BX8FH?PIO4Y!pv3lfGeSDf9;<=?pd< zdq_gd+57Q7BG9b`W0G7Y`W%dQCKj`8Vn+LVEg-?W6I$xuY{ z-POhlyH^!HaFA0sHuF2z*`*~}I;hIOGDA)U93j6kR7CPJ-IIp`8fcL6YL)vohkZBX z{mVE;qZ5T#30L7kpRCzO2mrzLHd#6K#}M1~czKe$p8_Y@umFsDW_-@#?(S};a%O)M z509PYqHALK?Zro_H{DPdsIO60o|rd$u_F3Xe(^>dWzxjntNy5 z&aF{x8}+iW^ZmU6>!1tb)v}s>5iT8Wrm;fG-q7Toc^0g4?Y2yCK*TSo1|X20it_oBWN?`8D!P{vWUeYL1*8p5>LCD!Wxpb*KvVZ`&vBU%`7I*qiZ={gbFgFC{IXSA^qB^8B_pXA~xu zVfBO;I5^r;8Moq0@678n(eapPbQtx8{3K0YB->*N2pyu;$f+o#EvAJRzFPYRS2yW z|9_FU^ZhQyS}~`2<7}P$odQUnoYI$A8+qm725p=Bf~u$plrEa8t#kLp;p}EZr65fq zw9C|z%30Sw2#JGQp^1x2Nm1Z>^3Jt6OeCCpWJA@*@SfQcAllXEEJu^h1RB!fLL20a zLPC^ApDQbK$QfvHGrTv2xV2;>soGL-1gu)h#+w4mwAsUT7bM<~{y#mvcRbbq8~=Yw z63QM)h-@i)L{8Z9M8@@z+1JiwiRDnCJNQ^M%S9di+Nl&OIAl)plb zS^l%uj>1q3x%t1D83&D1-D?jRWzq3(#d|)8mTV16EJ=+VYs9`iA~B&_3RWwy-bzOl zyE8|T3osR=X(XI~a}1%?;uKat(@gX3uiAPwv9`R-D`z_=)m!!8AeZ3wz3n#w5y*n? z&VN@;3{AV9f1&wG809{Uhc6^S>f4u=SbnNB^PU%mtn1b zqW_)xX>{$z|JpBNHaA9ay9DAlN{oHya8g3W^@)c=`?Rl63lavMJ%>UoM-hj+=o%}j z7Krxmm#_#S%MMfZ6W5coY_3~+y}zT^g;sJ%F7^GoO3bAFt}s8Hgpzb!#P!xTTjq1t z-Mmj?&FD|$rK&N+0fAD1W;$)Og@i6zYTSQxPD6T(-zC|c-{>6t%5TW;c|_uW8hyDG zY!?SiF(rG*^~qm}Oj;DJ+sc$A539!6=buhY_RyQsn-(qeJs>F?Pr(bWt*uBMP$pEB zIqh*b zspkz1w1(U9+)W=6p0p|-@e!yqJbcEP@boiI7mgy%adzWl%a3~Z*Hky)1#Ljr-$wX0 zY`G+8(`&(RoGov8@5=Xph({H+gkIgr*kP;kW`~;otg@}2Vo2T}%Xn)meeA2MXt}nx zPLT$({1OrX=CQ-zDhripD>dcsX3d^%xabb0P7ST?`?!mt`_@i!ALR7qP8KgbSP38` zB>W>ZB%i&_p4wy1mogD`<;ZX^=ijw6!LCx5QLZ`BdN02oRDtHxbIst{J;y`4$~BQl zj`L^zzJu#bSqjW#54oAM+&9WcofNe(xVA(IPgy*Fl-@g$tFaw)(53r+o>ZBamj@Br zxt0nVNeF7)hJOUzg2{`dR|Jt?ii$!BqzO4wlm>nIB+JIc0X4wRsP39L`hs(bF@tq#qjLrFebM+vr(tN@Jha1-5j;=XO%K@jE7c2sI=>ZP@lt8S;8pFS5ogdKeGXdH4`B(kWRNcSm1iA=#oOxO7R z=0Pr{!Znw-SkwBF65asW_EjXuqdqT@^3zugJ|eeA=o1h;5tLo__7!Oca^a=r0|XHX zxLJ3QW@TfGi;F)Brw4rbg6(L|L6Kywa(v2}zQpNWNAKqfJpw|+(7Qqlkuhzhfys8(@#=Q9HCMouy_gvT8dDj&@XD*Zy^pOGd2L zg{CYAMU_-KibET}+>xVycGmx5t7toZci{L#?oN8wVq#+A3i9>oWXqt1KsvNuPFaVQ z7pSSJH3~G##tKK*F8^?NUifj*MW?8i+lPukj-La6%%AQqk|qMfw!3;b3qxgY#O1co z+$+}%d^(qR;jS=dUqT}gTlAO$RrZz4kR!5Pl+T0;9Rjh`W;@<%%6C5fLZ&WHE!$(mTmLiYEacgPV41MqLH?RRu2?8HrrL`s3J^(I`JB2&KV ziVE3td<5b_G#YZf9!W^3_}rpAWkVp^Q90@bkM6K4=BR&p9(LMx32_JMZVGV0X`hoX zs~AgXXF;P=@NFP|G_<22tMMEZddk!19nO;~c^9AmLu!Go$&4>0A%XJ${{n)yA|5ng zW3dZd-wmR$R}fndc80*0(Qq_J$g3xms&8PBdk&Uc>UL0I&>e#Wv;*a{h74?a31(c# zge}*x1(cVirGipo*JAh?iFN}H;s;q45T;eQmVl3*iIjr8Smh~Kj z+R9zoIt__%xZPG^dr4`2uZB@9Hs63(ukOUen}5==i~B+mKCM&}SR?zx=MF`fOxx>Y z`oDJGeN&I#D#Tt=A701apIhVVegDJS(;W6DdhmHnoi9%6eDX+0Al+X1 zHI%BScW&fftFrBoZc)h)X}u6l?^#(q`wu6n!g+1ubKB0Vdzo>+tUf@%(aPS|tdz}@ z5FZh2{6l9e#z7;b*VX<%0v)^FbnE;TlfoN*FUw`eCZUH%jAo@SRK` z%*H-Dhlg?rr4|8AC@_(c_T+-nLXw&*^X=QWPOF2n(Gk%FqXe6TWV>-88aA(2#osK2 zTg362w1U5~pr~M$LqGHlQ2n0-<$=_TAcYfk)v zqVH6a!quAqT3V4z{{aAzFs0L~9A-IM!PlDsU3k0?0ga!=yI=&(rxFvU12;)XWF%E) z6$veE*a6GEa{h)ce4>3?kJqfmKK{^=i=-7+(foNW3F18pHqRr&vPfE5rq~GV)qJ>2 zdFnJ}vSy%}Mi5hyjD}HWJcQ)xqeQ{0ii%+h%nzL9zCcAWos5i8-q*W%*Ao56lhxG4 ztrK<~Zf;d15XEB$11m_+QXTbr34?@8jj>WQ*Ns0c(k|N|ISE0Cx^O5^%>SYdSbspf3R1)#7o-jnedT^l-|tq)-XqlGW-nqtSF?RCIy#g=Xk zSV;wHr3Rg9#~gOQpjA-ThN>vUGM6-&BU*F;8YCT60wc@M$*()`U15jSGVl$Il?0?b zNXxV^K&;XX;M}l@Pj7_(D2L<}>E-3sZ$!1+3>r&TQyzh7$?ldnGvFRJsB>ck7^?@6 z(k`9HQYs7m_$BwveQ!lA^0w%^6=KL4svHIHJapOzvgG=pgL<}aFLQ9g*LqQAKL3yh z+|FG|#WSd%`7(QzyvSKqBBI!T(+!3Kk5Ba>TS~CP2co>HNKRk)c5|UBL|s zd?!0|_h`Y;5a{A#LlA}1@Av@1iw1a0AUdjr!HIYlBMw>@@Gug?lXy6OxQ}YHBzDimCp0k7waY5PVC42%|SJ zFt9kfFYU{ZgzP*9Oo3Ee?{Alfj(hJ?gcbi(+M|D2^#D>DyvyQ`9{E;}^U9q9s- ze7=dE2}`nPMWz2;+~~r>mCObCp*gIy7B_e}{@N!X{wKTb$tCS8ik$p@=+N`$2F_I? zSszD7mqqh{l*N1Oc&!$fD9SGqg!$JOJkPCtYFy7r&z^_V^FQx0#Aga?###CMUkULN z1KTIP>K(214>69hzR+9 zvIjo;Z%PLBj@~Q%vs+&PbNj}6YbL{f+Iuhd=fhFZ2rU6?Z~LgDYz9xU1&D5{FPYqC zRJ-7z0l2k4SFSgrO-W(>l^V^?LGG33A_|0kRztbmb`(Roeb_xrs*;4;k=stZC1`P4 z?;Tv_hsXybEuhI(8lGQzv&?O@)IXaQZa-Ns3>Og`h4xdWJ)@I53#)JF#T>T2j$Y>F zM>lv05(Mt#8TtC3=V*jy>4%rafJGUmE;PbV0O3}`34i@X;rE35Zf;+)ez}fwrpa(V zeM*_>O9*F}V=qbUvKcGu!j_tGB~Mpb5}Mex1d|RmH3hsbP|sm`cweVZIz67+F)8uU zBI?Pm`z#m9t9gfhdjRBtF^uBf{JmM!J>ytrg%^a8F6Q5FYGVt`x}KNHzP)7mg_xni z3%mDhgeZlg!S(M%Alul@9cHl%Xnu7or3nPmoOSH%Xyo-C*XXiiw;yn8e~oTI13t9( z3j@-ZCQuTivGrs-y>b(*_7MS3kUai4^Q@04Ujq;_OZDjLfh<^k@PRVvJYMpKcsu+W zn`kyrPr>d6=`i808&h0MD9Pk&C5`BIUF;ENc{l0bMRG{2=1^7}3iB#9$L(kZ|G}8= z49PAKPoJGpJjNdHTZ0Skwr)E3=xsDa^6kJ@|5Nlc(V1qR%A@BmPmd!CuUX@Tk)27m z24g_G&~UzmS*t`ZL{u#t0%el_j!ZNEwHb>ad*wcC*btkME-m{z3PQDkdK9~W1$mY3 zODcAyzDQRx1Iy72pm1Dq#WXLWAt2{M5p6SZ1*(BsFHC7TCBa%4LC6#+PL}%6Xd)aB zqk?JYe*^u+to-$CAXb5W8T>ekM2YUq71Y|3ISIv-w$3_kV26E6?nV-9L#~FBd3m_jb=6Q)P>)!qhX;7eU#!-AUgQ76YerzJsd2Imsa=%DW`Q2wm`dN zZo=bij?!cl4M2#s-v#`{s%hmbqzNJnScTz5?6)!Q@K?R>n~r8O?)LNT7}T&(bfP73 zp)w7n6B9B_in#kT!x2k;>s7#AxIi7o6fu-=M}pRufsPmu9&ikq5zaJNg;-f9o$8T) z`6oQGv&@yjk$URW+hIuF2cp*(QT|~1wwbI+m{13lvq(e{T=^d%ih0+0%dlZ9h%^gGBoS!jYwm8BQ2fBfUK&6YaJcX{A8fCS>3ujYZ}39E)TRQ@Wn2NW+qXxKrz|4r;8$Ms;}pC4)ub6hk&yU^7v zuVj}UktYo%wmfe9!c2^vL$!C1Mk#*G@f!O040b)gV^7<3vb~jQI_2Eo+`MTv+EtXD2SV6rb za>c7L36zYOD>44VI@PjWCH^=1eSbW*{6PMRSwDqha$z>BP31>Ka{Q->><$f@$MuUv zJ$5nZJu3NHbfOa_+#_@#`D(criDH@OanO;3>0Nk$-~EK02sP{* zcjO3!Fs>bjHd=CpxV5JWjrfskCb7#Yu9Nu5;$!Pq;HGeN)DuoXa>OG8abbsMSfI{= zmmZJ`L0xR0KYr!Bh`m*lmUb|O;WiY2xnKP~$-i+Noy~h_7e1=@Iej|BWS9o=l9JCN znKj_O0;P1I5I7Z_;e)rPXS8lE=& zGXb){*qNRi;$9L1hE22mUc14gYw&&k<3E8qcYyeD^4G3LW5gv9g?iZk8P>TCT)Eka zF6<_66tSv(`Tb(0Q4Dja(sgLr27LrBt5@>L5w1Z5yalz}1;tXln;C{HYapEve z25W^tDPv18S;+SN>#GnAod%mEfH2)!zJ$**r7ef0vn6$p*q zjMc685@q9?Jyfo=hfN*kGk|T8a(C|B<0lAg?Ab=)1gT>g0L^a!3ERuWRnLKau9jhgrsVHKPm}d*2QFIw0;#UcgG&3(^ z(||6Q`I1Ldx2E~he<^i0$`}`->0X`hPRZ}u;NFP6=iRV6q1IIL#oci8(c$gfLZ139 z3OOB}_X-IES&ruJS8|CYC}uPUO_|7^#G_s2O4{g6npwrbZB-hbnXv`GRm{$62@AYI zU^eA_sDDOdOLovceI=K{=g3d+?%k#cWdR4ig3F!XUnPJ~|NB8Cyg{q2jg2!y zWBmAR6anWecZ5=C_Gj3{z)Yuf&;scL?Fv)f%_%-&L8*!CN&=)~Hx}74&Iwr1%V=K- zYcH?5z-y<8usfEttqr{N5N9BNDO!q%7eDGtF8Vfe9@rAjnDGRKSv`Y#vPBnpcxr!M9I|t%iTvS^hYot(~G58J+YuiViRdsT8~U#Kh~Ez->jz3 z&Jm)|W|Fzw71Jqe52x)b{o!VGau^zOoP5v26XpA8S~t?lXRP zH`A!GVSeBEXVm^=E07c75|Mz%L#fRw?!9>W$*&;Nh_;jbpy+9MASlQ~De-E*VTP-$ zJixEk&gdx$B}^K#)q!f{rFtZ^yR(zOo*Pw$OFEeeEdWLG+~2>o32PusFo;86yi55L z?f0wKl)TK@09pY0FMdRfXXK}$HYhBXljIsYpA$*O`aVmj3?7ceGVd{G$1(TpB|q+t zze-5OSt8N}A$@)KR;t>e?3bqV*QCz%B1!gfb`VbgA>F1q0`<47t@YE$d%L$6m|b&l zrMQSaG6e0XCZwgUoPEd~uODKnlU=&FD^=%gpFCl7fBEtyfc5SLgQECHZaeVBls~z= zJkw%my;ALmk9XVQtxO6i))w4J60V_aSj(NN)bKmkn8->n3KwlAm6597zTfX&JHd1M zYU(y7_OH%F&>X5=&1ko{xo9wN6VX|fMumoj`R>oIuZl1nsAc9Eb38z>*->VaDM>T` z*@<(Y8UoGne??Usp&=%M$MP2!1k-j5;zyF-kE?YEy7867=TAvPpB6(Yqkd#o>U!hn zxzpDE<2Ju}FCrw?ASNb(VdeXnwU2ne+ z_3?*`KL75Fc!^F?J4kYE0aP&|EP&_z`(M>Ok$M07^~<(%=T2DA6Ba7{-5AUv406(J zSNq3Ma5r@KGD$dRHVfynlC)`5Jo;T(-4sze!^8^{vXl)fg~d>=qOacHpiKnr-I!aq za`#jEK+@4IQw^5O(NFip#5_U8i#?`r<7H0x?_IVZK?=%s_0%t<#^i-1OyJG<-8lZ-Ofz2rS;Ku1$=RfC{Jv?okf@=bjG|MpE)@QE%zwJ@D?Xk)&1B8 z?Kbz8$XxKZ_1Fl*WN28rY7(FZ-C*c(BwO^mx zDE*3vB&kbh!)sBOI5eLuE-mTg z@Fd$q7El_cW4@+7M^M$w28LpLHpHLa*F3W!2gzSDk_m)142Cjt>Ib!K8DgS9fRt3t z=aY8%3|Lt}cl`+?~SzrPIJs+QrQ=HKvqY z%G}tsKQ-mv`fHCap~1_<6{2GyHdG(M+s=A%8c1QMtCr1v{NPP>$XRIGbjkS>#Glki zjoZJt9k>2;&QoW+|MXY+ORxBDcRL|Upkb-zwqHm>_fNdP@fs?O{AwW#6|SV53(Q;G z@bL8I$9r^HTi+`S0&;gWnzMu!b5rdyqpC&Su52!5Q^b;M&B}|`8J%~Wl&$;{a9P*> z%(pWU!j>*#HzD$Q@rVS&Po63u%_7W0=e`g#J0i!)!&X98fRhzFt_JG=cyuNao+$;A zG^6`27grb^he5ne=K~G{{pU6A|Lir@R!i+R{xoK0LU&!WL}my|Z#=@dYFI<38^-*%U9*bxO%r=d!@E(^JKAqTbo}jaE=@3lsfN5K6H-bCoZCmGxABW z5|)`ws_kgLMNZxn6oYNs)`PAKw-aP|ZL%^`_pov3Gp*L(cPJ_+TpW;T5-suV%IdPe z7F)Jr8g(ABA4+=^3h;ECeyi72WM>PM{~Vxf9Fih_)U25{X)gg`%9sZtB_SBbO;&R2 zt>QWI5US1VzMpaz>D*@geu?sGU3_S^lj1S_!b$*8wuU>w=Y1<5=2n-H+a?g675)BQFpuT7QSMkGAQw`!uy%2J3a?+S+s4yb9 z*xK6KT3T6j1jf`VyuAlH_gdSIn>18Zh@|Y>l|aiX+A;%*y>I6>_qn-yKIy)Gz1{fX zrB^AEv05Z5%eFEkSYBJrN8RUXa8y`G4OX+F-U9Qr*!;rzv1GJq08dj3LI^Q&)e(pC zL0jL&R_n_SCUM1-A^y!9QwLuNHrn2q-|iE?ur9ULsr zyp9a%{WnO6JLRV(+G*p(d~o8LUX)Hug4YZyDiF7M`Ex`wjbMUj%m$oHZ$u}-@^ z!u;g@-SA#KfNEn9=fkYf^+OgU?`BedbB4hXA|)jSmSL&f!-lx|-n!$$p-AtADcZus zo1&YkTGN}SW5L8_e_yQ^o-YXf35<*G=7lo+b5YHwf&iLU=4`md%W|4|(XiY!BX{Be z%#%_vSrN5E7wiOYBmN_ zjie^^Q>D(Sfi%ZXL$q;ZjxMR}+qulxZvH;gk%)^A58R2nr~Pf+_auCVV!h9oHMOlG zHkKFdbBO)S(6ImV-0(bkMnN&mW1r!)bNqY#Ii?sSL`8ZD=NRFJ_hWg6XTw?l+7&d+ zw~td9&Ta}|%UHUeFCEaFt#dJKH=ag#?9pPnyBH2=#qE@RVp4jN1<~kID`!6pcIU*c zKIfa0STP~N=mDL;K>;=LsOoS_beGEQFiqm*@A9%?7aiYRm>Jje@$`O&wEu9!znj?b z2%@0Vo|6V;hqR_E?5x@sS6coGANu?2leW*F zX3_=2N8%=V<<`Gu+$>y=Cb{rvhl{1UV}fiVbWmgce&5#(XkYxZPk%_Emm*Zj(%}(bz{C4o!E^w?ucXy}6&r9mV zBqoY_bC%7*57t>2NWU>Xz}$ELYp*|nIve{yo==wNphPfEgihEhn(ONbDG>#E&HI_bjtyq5`{_@x2iu}by;E}%MhpG*rf!k@9aE1) z?fdX2>-H2+6(H&0$Y%kylNYVburS~3x1xR)e{gI?V<;U|)av$}JH;~3Ie=L|ZSNuEs7c_Tq!1ksatp zoWyZvec=1lM0_+0V>R_6I+hA15~S7Y6Mn=;yt|B|M%m;U?U%l|KUA)v%l86bIHWE7 z=HueEoz)KvNOK&TKWah>%iiT*ypA88F~hlOM=zk*h+FS46x#0ePLIuP2aQI>*90ag z6b3Ur_SWMH4peP^nVN*zWy>9BUm5#?Q^KvX9M2(cJZf7ivr<^j6t{qLasd8_C6m#jg>x#q4oNkMA?r|JO&Z3w{x;VBcQ~qP*?m*33`+uj(hBfkoojLOmTNLw*MUqPkUh)XS z?i-gKf5%-Q1W_Iv6=?F;P;T1c<@p_A5~yFNXW3? zy`t8AWtz>Pxs%i9NvAIV%WflqU}|?U(_;FxrIi%fIuqTki6f5xcM*JNjGjlXiY{0l z?3GNPfAgJ2|L^L&!*=+zzdh4%d0Bg4^pW>E!sy!{-|RAY*Ch-g8h+JlG2iw2x&O)M z@?)ghd!L6>x4oxllM%*V7R$6gInn;s1Y_XZd9%2Pk2A0~|>5Wfac zbrNZM_O1;E-IR>!Q^i#FhgZ{!D{&CRlF01?CwEsK$FgxxSDyI!Lzm)n^UCx6O?Jd% z{S`KeDf(&j{Wn;jx+y$FyCEn_qGb>WO-c9#MHa||;5+_b=DZh9ek(zk_bbVF2>78O Mqas}-Y2^R^0VGg_TmS$7 diff --git a/index.html b/index.html index 894ce1b875e..63d607d53d9 100644 --- a/index.html +++ b/index.html @@ -8,8 +8,6 @@ -
@@ -30,5 +28,35 @@ + + From cdc6d5ca45e64a0cf21b7ee0f13aa993568cb428 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 15 Feb 2024 22:18:09 +0100 Subject: [PATCH 034/123] chore: remove sinon dev dependency (#2767) --- package.json | 1 - test/no-strict-content-length.js | 17 +++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0c42aa0696f..c85ec55102f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "proxy": "^1.0.2", "proxyquire": "^2.1.3", "request": "^2.88.2", - "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", "superagent": "^8.1.2", diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index d74f4677d56..aa3d1725cb2 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -1,23 +1,28 @@ 'use strict' const { tspl } = require('@matteo.collina/tspl') +const { ok } = require('node:assert') const { test, after, describe } = require('node:test') const { Client } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') -const sinon = require('sinon') const { wrapWithAsyncIterable } = require('./utils/async-iterators') -describe('strictContentLength: false', (t) => { - const emitWarningStub = sinon.stub(process, 'emitWarning') +describe('strictContentLength: false', () => { + const emitWarningOriginal = process.emitWarning + let emitWarningCalled = false + + process.emitWarning = function () { + emitWarningCalled = true + } function assertEmitWarningCalledAndReset () { - sinon.assert.called(emitWarningStub) - emitWarningStub.resetHistory() + ok(emitWarningCalled) + emitWarningCalled = false } after(() => { - emitWarningStub.restore() + process.emitWarning = emitWarningOriginal }) test('request invalid content-length', async (t) => { From db5fd979f7e855033f358b8cf8c49c74d0e0f460 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 17 Feb 2024 05:31:58 +0100 Subject: [PATCH 035/123] tests: skip test/node-test/debug on node 21.6.2 and windows (#2765) --- test/node-test/debug.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/node-test/debug.js b/test/node-test/debug.js index 055b2a121df..ade1843ec36 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -8,7 +8,10 @@ const { tspl } = require('@matteo.collina/tspl') // eslint-disable-next-line no-control-regex const removeEscapeColorsRE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g -test('debug#websocket', async t => { +// see https://github.com/nodejs/node/issues/51766 +const skip = process.version === 'v21.6.2' && process.platform === 'win32' + +test('debug#websocket', { skip }, async t => { const assert = tspl(t, { plan: 8 }) const child = spawn( process.execPath, @@ -48,7 +51,7 @@ test('debug#websocket', async t => { child.kill() }) -test('debug#fetch', async t => { +test('debug#fetch', { skip }, async t => { const assert = tspl(t, { plan: 7 }) const child = spawn( process.execPath, @@ -83,7 +86,7 @@ test('debug#fetch', async t => { child.kill() }) -test('debug#undici', async t => { +test('debug#undici', { skip }, async t => { // Due to Node.js webpage redirect const assert = tspl(t, { plan: 7 }) const child = spawn( From eab6a0d26f3063b94179a65246010ea3040ea666 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 17 Feb 2024 08:03:00 +0100 Subject: [PATCH 036/123] chore: improve usage of skip in tests (#2761) --- test/cookie/global-headers.js | 19 ++++++------------- test/fetch/http2.js | 3 --- test/gc.js | 10 +++------- test/node-test/autoselectfamily.js | 16 ++++++---------- .../diagnostics-channel/connect-error.js | 13 ++----------- test/node-test/diagnostics-channel/error.js | 13 ++----------- test/node-test/diagnostics-channel/get.js | 13 ++----------- .../diagnostics-channel/post-stream.js | 13 ++----------- test/node-test/diagnostics-channel/post.js | 13 ++----------- 9 files changed, 25 insertions(+), 88 deletions(-) diff --git a/test/cookie/global-headers.js b/test/cookie/global-headers.js index e78566905a1..1dc9d9a5857 100644 --- a/test/cookie/global-headers.js +++ b/test/cookie/global-headers.js @@ -1,6 +1,6 @@ 'use strict' -const { test, skip } = require('node:test') +const { describe, test } = require('node:test') const assert = require('node:assert') const { deleteCookie, @@ -10,15 +10,8 @@ const { } = require('../..') const { getHeadersList } = require('../../lib/cookies/util') -/* global Headers */ - -if (!globalThis.Headers) { - skip('No global Headers to test') - process.exit(0) -} - -test('Using global Headers', async (t) => { - await t.test('deleteCookies', () => { +describe('Using global Headers', async () => { + test('deleteCookies', () => { const headers = new Headers() assert.equal(headers.get('set-cookie'), null) @@ -26,7 +19,7 @@ test('Using global Headers', async (t) => { assert.equal(headers.get('set-cookie'), 'undici=; Expires=Thu, 01 Jan 1970 00:00:00 GMT') }) - await t.test('getCookies', () => { + test('getCookies', () => { const headers = new Headers({ cookie: 'get=cookies; and=attributes' }) @@ -34,7 +27,7 @@ test('Using global Headers', async (t) => { assert.deepEqual(getCookies(headers), { get: 'cookies', and: 'attributes' }) }) - await t.test('getSetCookies', () => { + test('getSetCookies', () => { const headers = new Headers({ 'set-cookie': 'undici=getSetCookies; Secure' }) @@ -54,7 +47,7 @@ test('Using global Headers', async (t) => { } }) - await t.test('setCookie', () => { + test('setCookie', () => { const headers = new Headers() setCookie(headers, { name: 'undici', value: 'setCookie' }) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 0562730b26b..0793a21556c 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -14,8 +14,6 @@ const { Client, fetch, Headers } = require('../..') const { closeClientAndServerAsPromise } = require('../utils/node-http') -const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) - test('[Fetch] Issue#2311', async (t) => { const expectedBody = 'hello from client!' @@ -180,7 +178,6 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) // Skipping for now, there is something odd in the way the body is handled test( '[Fetch] Should handle h2 request with body (stream)', - { skip: nodeVersion === 16 }, async (t) => { const server = createSecureServer(pem) const expectedBody = readFileSync(__filename, 'utf-8') diff --git a/test/gc.js b/test/gc.js index 91ea6ab170c..1a0bf8bcb96 100644 --- a/test/gc.js +++ b/test/gc.js @@ -6,17 +6,13 @@ const { test, after } = require('node:test') const { createServer } = require('node:net') const { Client, Pool } = require('..') -const SKIP = ( - typeof WeakRef === 'undefined' || - typeof FinalizationRegistry === 'undefined' || - typeof global.gc === 'undefined' -) +const skip = typeof global.gc === 'undefined' setInterval(() => { global.gc() }, 100).unref() -test('gc should collect the client if, and only if, there are no active sockets', { skip: SKIP }, async t => { +test('gc should collect the client if, and only if, there are no active sockets', { skip }, async t => { t = tspl(t, { plan: 4 }) const server = createServer((socket) => { @@ -60,7 +56,7 @@ test('gc should collect the client if, and only if, there are no active sockets' await t.completed }) -test('gc should collect the pool if, and only if, there are no active sockets', { skip: SKIP }, async t => { +test('gc should collect the pool if, and only if, there are no active sockets', { skip }, async t => { t = tspl(t, { plan: 4 }) const server = createServer((socket) => { diff --git a/test/node-test/autoselectfamily.js b/test/node-test/autoselectfamily.js index 678a8959042..196f219fc79 100644 --- a/test/node-test/autoselectfamily.js +++ b/test/node-test/autoselectfamily.js @@ -1,6 +1,6 @@ 'use strict' -const { test, skip } = require('node:test') +const { test } = require('node:test') const dgram = require('node:dgram') const { Resolver } = require('node:dns') const dnsPacket = require('dns-packet') @@ -16,11 +16,7 @@ const { tspl } = require('@matteo.collina/tspl') * explicitly passed in tests in this file to avoid compatibility problems across release lines. * */ - -if (!nodeHasAutoSelectFamily) { - skip('autoSelectFamily is not supportee') - process.exit() -} +const skip = !nodeHasAutoSelectFamily function _lookup (resolver, hostname, options, cb) { resolver.resolve(hostname, 'ANY', (err, replies) => { @@ -68,7 +64,7 @@ function createDnsServer (ipv6Addr, ipv4Addr, cb) { }) } -test('with autoSelectFamily enable the request succeeds when using request', async (t) => { +test('with autoSelectFamily enable the request succeeds when using request', { skip }, async (t) => { const p = tspl(t, { plan: 3 }) createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { @@ -108,7 +104,7 @@ test('with autoSelectFamily enable the request succeeds when using request', asy await p.completed }) -test('with autoSelectFamily enable the request succeeds when using a client', async (t) => { +test('with autoSelectFamily enable the request succeeds when using a client', { skip }, async (t) => { const p = tspl(t, { plan: 3 }) createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { @@ -149,7 +145,7 @@ test('with autoSelectFamily enable the request succeeds when using a client', as await p.completed }) -test('with autoSelectFamily disabled the request fails when using request', async (t) => { +test('with autoSelectFamily disabled the request fails when using request', { skip }, async (t) => { const p = tspl(t, { plan: 1 }) createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { @@ -177,7 +173,7 @@ test('with autoSelectFamily disabled the request fails when using request', asyn await p.completed }) -test('with autoSelectFamily disabled the request fails when using a client', async (t) => { +test('with autoSelectFamily disabled the request fails when using a client', { skip }, async (t) => { const p = tspl(t, { plan: 1 }) createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { diff --git a/test/node-test/diagnostics-channel/connect-error.js b/test/node-test/diagnostics-channel/connect-error.js index ecab3856b89..38dbd9d717f 100644 --- a/test/node-test/diagnostics-channel/connect-error.js +++ b/test/node-test/diagnostics-channel/connect-error.js @@ -1,17 +1,8 @@ 'use strict' -const { test, skip } = require('node:test') +const { test } = require('node:test') const { tspl } = require('@matteo.collina/tspl') - -let diagnosticsChannel - -try { - diagnosticsChannel = require('node:diagnostics_channel') -} catch { - skip('missing diagnostics_channel') - process.exit(0) -} - +const diagnosticsChannel = require('node:diagnostics_channel') const { Client } = require('../../..') test('Diagnostics channel - connect error', (t) => { diff --git a/test/node-test/diagnostics-channel/error.js b/test/node-test/diagnostics-channel/error.js index b4861b8f71c..ce6e0978146 100644 --- a/test/node-test/diagnostics-channel/error.js +++ b/test/node-test/diagnostics-channel/error.js @@ -1,17 +1,8 @@ 'use strict' -const { test, skip, after } = require('node:test') +const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') - -let diagnosticsChannel - -try { - diagnosticsChannel = require('node:diagnostics_channel') -} catch { - skip('missing diagnostics_channel') - process.exit(0) -} - +const diagnosticsChannel = require('node:diagnostics_channel') const { Client } = require('../../..') const { createServer } = require('node:http') diff --git a/test/node-test/diagnostics-channel/get.js b/test/node-test/diagnostics-channel/get.js index cde68a46f2c..3366481910b 100644 --- a/test/node-test/diagnostics-channel/get.js +++ b/test/node-test/diagnostics-channel/get.js @@ -1,17 +1,8 @@ 'use strict' -const { test, skip, after } = require('node:test') +const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') - -let diagnosticsChannel - -try { - diagnosticsChannel = require('node:diagnostics_channel') -} catch { - skip('missing diagnostics_channel') - process.exit(0) -} - +const diagnosticsChannel = require('node:diagnostics_channel') const { Client } = require('../../..') const { createServer } = require('node:http') diff --git a/test/node-test/diagnostics-channel/post-stream.js b/test/node-test/diagnostics-channel/post-stream.js index a50d5ea02c0..49fa0be1a04 100644 --- a/test/node-test/diagnostics-channel/post-stream.js +++ b/test/node-test/diagnostics-channel/post-stream.js @@ -1,18 +1,9 @@ 'use strict' -const { test, skip, after } = require('node:test') +const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') const { Readable } = require('node:stream') - -let diagnosticsChannel - -try { - diagnosticsChannel = require('node:diagnostics_channel') -} catch { - skip('missing diagnostics_channel') - process.exit(0) -} - +const diagnosticsChannel = require('node:diagnostics_channel') const { Client } = require('../../..') const { createServer } = require('node:http') diff --git a/test/node-test/diagnostics-channel/post.js b/test/node-test/diagnostics-channel/post.js index 45368e3fdab..cddb22ace17 100644 --- a/test/node-test/diagnostics-channel/post.js +++ b/test/node-test/diagnostics-channel/post.js @@ -1,17 +1,8 @@ 'use strict' -const { test, skip, after } = require('node:test') +const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') - -let diagnosticsChannel - -try { - diagnosticsChannel = require('node:diagnostics_channel') -} catch { - skip('missing diagnostics_channel') - process.exit(0) -} - +const diagnosticsChannel = require('node:diagnostics_channel') const { Client } = require('../../../') const { createServer } = require('node:http') From d3128c1cee7e485cff3d57c5b75f78e1e271e8d0 Mon Sep 17 00:00:00 2001 From: Lorenzo Rossi <65499789+rossilor95@users.noreply.github.com> Date: Sat, 17 Feb 2024 08:03:39 +0100 Subject: [PATCH 037/123] feat: improve mock error breadcrumbs (#2774) --- lib/mock/mock-utils.js | 7 +++--- test/mock-agent.js | 8 +++---- test/mock-utils.js | 54 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 96282f0ec9d..036d39680ff 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -138,19 +138,20 @@ function getMockDispatch (mockDispatches, key) { // Match method matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) if (matchedMockDispatches.length === 0) { - throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`) } // Match body matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) if (matchedMockDispatches.length === 0) { - throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`) } // Match headers matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) if (matchedMockDispatches.length === 0) { - throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`) } return matchedMockDispatches[0] diff --git a/test/mock-agent.js b/test/mock-agent.js index 80b04e658e8..cd0cf55f587 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2176,7 +2176,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for meth await t.rejects(request(`${baseUrl}/foo`, { method: 'WRONG' - }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => { @@ -2209,7 +2209,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for body await t.rejects(request(`${baseUrl}/foo`, { method: 'GET', body: 'wrong' - }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => { @@ -2246,7 +2246,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for head headers: { 'User-Agent': 'wrong' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) }) test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => { @@ -2317,7 +2317,7 @@ test('MockAgent - headers function interceptor', async (t) => { headers: { Authorization: 'Bearer foo' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) { const { statusCode } = await request(`${baseUrl}/foo`, { diff --git a/test/mock-utils.js b/test/mock-utils.js index baf0933ba57..744011c70b8 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -87,6 +87,60 @@ describe('getMockDispatch', () => { method: 'wrong' }), new MockNotMatchedError('Mock dispatch not matched for path \'wrong\'')) }) + + test('it should throw if no dispatch matches method', (t) => { + t = tspl(t, { plan: 1 }) + const dispatches = [ + { + path: 'path', + method: 'method', + consumed: false + } + ] + + t.throws(() => getMockDispatch(dispatches, { + path: 'path', + method: 'wrong' + }), new MockNotMatchedError('Mock dispatch not matched for method \'wrong\' on path \'path\'')) + }) + + test('it should throw if no dispatch matches body', (t) => { + t = tspl(t, { plan: 1 }) + const dispatches = [ + { + path: 'path', + method: 'method', + body: 'body', + consumed: false + } + ] + + t.throws(() => getMockDispatch(dispatches, { + path: 'path', + method: 'method', + body: 'wrong' + }), new MockNotMatchedError('Mock dispatch not matched for body \'wrong\' on path \'path\'')) + }) + + test('it should throw if no dispatch matches headers', (t) => { + t = tspl(t, { plan: 1 }) + const dispatches = [ + { + path: 'path', + method: 'method', + body: 'body', + headers: { key: 'value' }, + consumed: false + } + ] + + t.throws(() => getMockDispatch(dispatches, { + path: 'path', + method: 'method', + body: 'body', + headers: { key: 'wrong' } + }), new MockNotMatchedError('Mock dispatch not matched for headers \'{"key":"wrong"}\' on path \'path\'')) + }) }) describe('getResponseData', () => { From e1195cbf32cb5f10f25e820d580264f24c7edc71 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 17 Feb 2024 14:20:31 -0500 Subject: [PATCH 038/123] expose MessageEvent in fetch bundle (#2770) * expose MessageEvent * fixup --- index-fetch.js | 1 + index.js | 4 + test/websocket/messageevent.js | 136 +++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/websocket/messageevent.js diff --git a/index-fetch.js b/index-fetch.js index 41cbd781b55..851731865b5 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -16,5 +16,6 @@ module.exports.Response = require('./lib/fetch/response').Response module.exports.Request = require('./lib/fetch/request').Request module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket +module.exports.MessageEvent = require('./lib/websocket/events').MessageEvent module.exports.EventSource = require('./lib/eventsource/eventsource').EventSource diff --git a/index.js b/index.js index bf46fc08d98..2e274b24029 100644 --- a/index.js +++ b/index.js @@ -137,7 +137,11 @@ const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL') module.exports.parseMIMEType = parseMIMEType module.exports.serializeAMimeType = serializeAMimeType +const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/websocket/events') module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket +module.exports.CloseEvent = CloseEvent +module.exports.ErrorEvent = ErrorEvent +module.exports.MessageEvent = MessageEvent module.exports.request = makeDispatcher(api.request) module.exports.stream = makeDispatcher(api.stream) diff --git a/test/websocket/messageevent.js b/test/websocket/messageevent.js new file mode 100644 index 00000000000..372cdd2ba89 --- /dev/null +++ b/test/websocket/messageevent.js @@ -0,0 +1,136 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('assert') +const { MessageEvent } = require('../..') + +test('test/parallel/test-messageevent-brandcheck.js', () => { + [ + 'data', + 'origin', + 'lastEventId', + 'source', + 'ports' + ].forEach((i) => { + assert.throws(() => Reflect.get(MessageEvent.prototype, i, {}), { + constructor: TypeError, + message: 'Illegal invocation' + }) + }) +}) + +test('test/parallel/test-worker-message-port.js', () => { + const dummyPort = new MessageChannel().port1 + + for (const [args, expected] of [ + [ + ['message'], + { + type: 'message', + data: null, + origin: '', + lastEventId: '', + source: null, + ports: [] + } + ], + [ + ['message', { data: undefined, origin: 'foo' }], + { + type: 'message', + data: null, + origin: 'foo', + lastEventId: '', + source: null, + ports: [] + } + ], + [ + ['message', { data: 2, origin: 1, lastEventId: 0 }], + { + type: 'message', + data: 2, + origin: '1', + lastEventId: '0', + source: null, + ports: [] + } + ], + [ + ['message', { lastEventId: 'foo' }], + { + type: 'message', + data: null, + origin: '', + lastEventId: 'foo', + source: null, + ports: [] + } + ], + [ + ['messageerror', { lastEventId: 'foo', source: dummyPort }], + { + type: 'messageerror', + data: null, + origin: '', + lastEventId: 'foo', + source: dummyPort, + ports: [] + } + ], + [ + ['message', { ports: [dummyPort], source: null }], + { + type: 'message', + data: null, + origin: '', + lastEventId: '', + source: null, + ports: [dummyPort] + } + ] + ]) { + const ev = new MessageEvent(...args) + const { type, data, origin, lastEventId, source, ports } = ev + assert.deepStrictEqual(expected, { + type, data, origin, lastEventId, source, ports + }) + } + + assert.throws(() => new MessageEvent('message', { source: 1 }), { + constructor: TypeError, + message: 'MessagePort: Expected 1 to be an instance of MessagePort.' + }) + assert.throws(() => new MessageEvent('message', { source: {} }), { + constructor: TypeError, + message: 'MessagePort: Expected [object Object] to be an instance of MessagePort.' + }) + assert.throws(() => new MessageEvent('message', { ports: 0 }), { + constructor: TypeError, + message: 'Sequence: Value of type Number is not an Object.' + }) + assert.throws(() => new MessageEvent('message', { ports: [null] }), { + constructor: TypeError, + message: 'MessagePort: Expected null to be an instance of MessagePort.' + }) + assert.throws(() => + new MessageEvent('message', { ports: [{}] }) + , { + constructor: TypeError, + message: 'MessagePort: Expected [object Object] to be an instance of MessagePort.' + }) + + assert(new MessageEvent('message') instanceof Event) + + // https://github.com/nodejs/node/issues/51767 + const event = new MessageEvent('type', { cancelable: true }) + event.preventDefault() + + assert(event.cancelable) + assert(event.defaultPrevented) +}) + +test('bug in node core', () => { + // In node core, this will throw an error. + new MessageEvent('', null) // eslint-disable-line no-new +}) From 2144a3d5fb69fe505fa38ffb7882223b0fb3d00b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 18 Feb 2024 18:17:02 +0100 Subject: [PATCH 039/123] test: always exit with 0 when running in Node's Daily WPT Report CI job (#2778) --- test/wpt/runner/runner.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs index cbd5ea540d8..a24576f7415 100644 --- a/test/wpt/runner/runner.mjs +++ b/test/wpt/runner/runner.mjs @@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' import { colors, handlePipes, normalizeName, parseMeta, resolveStatusPath } from './util.mjs' +const alwaysExit0 = process.env.GITHUB_WORKFLOW === 'Daily WPT report' + const basePath = fileURLToPath(join(import.meta.url, '../..')) const testPath = join(basePath, 'tests') const statusPath = join(basePath, 'status') @@ -343,7 +345,11 @@ export class WPTRunner extends EventEmitter { `unexpected failures: ${failedTests - expectedFailures}` ) - process.exit(failedTests - expectedFailures ? 1 : process.exitCode) + if (alwaysExit0) { + process.exit(0) + } else { + process.exit(failedTests - expectedFailures ? 1 : process.exitCode) + } } addInitScript (code) { From 71c1f206f2bc03b1c78a8a960915d2d8862769f0 Mon Sep 17 00:00:00 2001 From: Martin DONADIEU Date: Mon, 19 Feb 2024 00:11:26 +0100 Subject: [PATCH 040/123] fix: add node prefix for util to fix issue in env with min version node 18 (#2775) * fix: add node prefix for util to fix issue in env with min version node 18 * fix: add missing prefix --- lib/fetch/body.js | 2 +- lib/fetch/util.js | 2 +- test/fetch/pull-dont-push.js | 8 ++++---- test/websocket/messageevent.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 2781a7b90d9..65fd63c5b23 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -17,7 +17,7 @@ const { webidl } = require('./webidl') const { Blob, File: NativeFile } = require('node:buffer') const assert = require('node:assert') const { isErrored } = require('../core/util') -const { isArrayBuffer } = require('util/types') +const { isArrayBuffer } = require('node:util/types') const { File: UndiciFile } = require('./file') const { serializeAMimeType } = require('./dataURL') const { Readable } = require('node:stream') diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 82e96ec9acd..fdda578213e 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -8,7 +8,7 @@ const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, pa const { performance } = require('node:perf_hooks') const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../core/util') const assert = require('node:assert') -const { isUint8Array } = require('util/types') +const { isUint8Array } = require('node:util/types') const { webidl } = require('./webidl') // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable diff --git a/test/fetch/pull-dont-push.js b/test/fetch/pull-dont-push.js index 5dbf331f397..58eaee3e8fd 100644 --- a/test/fetch/pull-dont-push.js +++ b/test/fetch/pull-dont-push.js @@ -3,10 +3,10 @@ const { test } = require('node:test') const assert = require('node:assert') const { fetch } = require('../..') -const { createServer } = require('http') -const { once } = require('events') -const { Readable, pipeline } = require('stream') -const { setTimeout: sleep } = require('timers/promises') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { Readable, pipeline } = require('node:stream') +const { setTimeout: sleep } = require('node:timers/promises') const { closeServerAsPromise } = require('../utils/node-http') diff --git a/test/websocket/messageevent.js b/test/websocket/messageevent.js index 372cdd2ba89..e1ab3d16dd7 100644 --- a/test/websocket/messageevent.js +++ b/test/websocket/messageevent.js @@ -1,7 +1,7 @@ 'use strict' const { test } = require('node:test') -const assert = require('assert') +const assert = require('node:assert') const { MessageEvent } = require('../..') test('test/parallel/test-messageevent-brandcheck.js', () => { From d4ce0b19d86e6446d17897743d0ad1eda8079200 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 19 Feb 2024 07:42:22 +0100 Subject: [PATCH 041/123] perf: improve perf of parseRawHeaders (#2781) --- benchmarks/parseRawHeaders.mjs | 24 ++++++++++++++++++++++++ lib/core/util.js | 26 +++++++++++++++++--------- test/node-test/util.js | 1 + 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 benchmarks/parseRawHeaders.mjs diff --git a/benchmarks/parseRawHeaders.mjs b/benchmarks/parseRawHeaders.mjs new file mode 100644 index 00000000000..b7ac0f92586 --- /dev/null +++ b/benchmarks/parseRawHeaders.mjs @@ -0,0 +1,24 @@ +import { bench, group, run } from 'mitata' +import { parseRawHeaders } from '../lib/core/util.js' + +const rawHeadersMixed = ['key', 'value', Buffer.from('key'), Buffer.from('value')] +const rawHeadersOnlyStrings = ['key', 'value', 'key', 'value'] +const rawHeadersOnlyBuffers = [Buffer.from('key'), Buffer.from('value'), Buffer.from('key'), Buffer.from('value')] +const rawHeadersContent = ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"'] + +group('parseRawHeaders', () => { + bench('only strings', () => { + parseRawHeaders(rawHeadersOnlyStrings) + }) + bench('only buffers', () => { + parseRawHeaders(rawHeadersOnlyBuffers) + }) + bench('mixed', () => { + parseRawHeaders(rawHeadersMixed) + }) + bench('content-disposition special case', () => { + parseRawHeaders(rawHeadersContent) + }) +}) + +await run() diff --git a/lib/core/util.js b/lib/core/util.js index 1ad0eab89fd..96e76cc1355 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -279,22 +279,30 @@ function parseHeaders (headers, obj) { } function parseRawHeaders (headers) { - const ret = [] + const len = headers.length + const ret = new Array(len) + let hasContentLength = false let contentDispositionIdx = -1 + let key + let val + let kLen = 0 for (let n = 0; n < headers.length; n += 2) { - const key = headers[n + 0].toString() - const val = headers[n + 1].toString('utf8') + key = headers[n] + val = headers[n + 1] + + typeof key !== 'string' && (key = key.toString()) + typeof val !== 'string' && (val = val.toString('utf8')) - if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { - ret.push(key, val) + kLen = key.length + if (kLen === 14 && key[7] === '-' && (key === 'content-length' || key.toLowerCase() === 'content-length')) { hasContentLength = true - } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { - contentDispositionIdx = ret.push(key, val) - 1 - } else { - ret.push(key, val) + } else if (kLen === 19 && key[7] === '-' && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = n + 1 } + ret[n] = key + ret[n + 1] = val } // See https://github.com/nodejs/node/pull/46528 diff --git a/test/node-test/util.js b/test/node-test/util.js index 9d18f98d596..fa1c6c50eb4 100644 --- a/test/node-test/util.js +++ b/test/node-test/util.js @@ -89,6 +89,7 @@ test('parseHeaders', () => { test('parseRawHeaders', () => { assert.deepEqual(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value']) + assert.deepEqual(util.parseRawHeaders(['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']), ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']) }) test('buildURL', () => { From fedae35f1b71999d2327ef6a28538f5845a71515 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 19 Feb 2024 15:53:14 +0100 Subject: [PATCH 042/123] fix: make mock-agent.js test more resilient (#2780) --- test/mock-agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mock-agent.js b/test/mock-agent.js index cd0cf55f587..58066229f58 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -789,7 +789,7 @@ test('MockAgent - handle delays to simulate work', async (t) => { const response = await getResponse(body) t.strictEqual(response, 'hello') - const elapsedInMs = process.hrtime(start)[1] / 1e6 + const elapsedInMs = Math.ceil(process.hrtime(start)[1] / 1e6) t.ok(elapsedInMs >= 50, `Elapsed time is not greater than 50ms: ${elapsedInMs}`) }) From ff4c57a3d336afface36e0d81c3b025680618986 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 20 Feb 2024 08:04:15 +0100 Subject: [PATCH 043/123] chore: make some test run even without internet connection (#2786) --- test/client-node-max-header-size.js | 59 ++++++++++++++++++++--------- test/connect-timeout.js | 2 +- test/issue-1670.js | 2 +- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/test/client-node-max-header-size.js b/test/client-node-max-header-size.js index 227f6705084..379c5e6970a 100644 --- a/test/client-node-max-header-size.js +++ b/test/client-node-max-header-size.js @@ -1,21 +1,44 @@ 'use strict' -const { execSync } = require('node:child_process') -const { throws, doesNotThrow } = require('node:assert') -const { test } = require('node:test') - -const command = 'node -e "require(\'.\').request(\'https://httpbin.org/get\')"' - -test("respect Node.js' --max-http-header-size", () => { - throws( - () => execSync(`${command} --max-http-header-size=1`, { stdio: 'pipe' }), - /UND_ERR_HEADERS_OVERFLOW/, - 'max-http-header-size=1 should throw' - ) - - doesNotThrow( - () => execSync(command), - /UND_ERR_HEADERS_OVERFLOW/, - 'default max-http-header-size should not throw' - ) +const { tspl } = require('@matteo.collina/tspl') +const { once } = require('node:events') +const { exec } = require('node:child_process') +const { test, before, after, describe } = require('node:test') +const { createServer } = require('node:http') + +describe("Node.js' --max-http-header-size cli option", () => { + let server + + before(async () => { + server = createServer((req, res) => { + res.writeHead(200, 'OK', { + 'Content-Length': 2 + }) + res.write('OK') + res.end() + }).listen(0) + + await once(server, 'listening') + }) + + after(() => server.close()) + + test("respect Node.js' --max-http-header-size", async (t) => { + t = tspl(t, { plan: 6 }) + const command = 'node -e "require(\'.\').request(\'http://localhost:' + server.address().port + '\')"' + + exec(`${command} --max-http-header-size=1`, { stdio: 'pipe' }, (err, stdout, stderr) => { + t.strictEqual(err.code, 1) + t.strictEqual(stdout, '') + t.match(stderr, /UND_ERR_HEADERS_OVERFLOW/, '--max-http-header-size=1 should throw') + }) + + exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { + t.ifError(err) + t.strictEqual(stdout, '') + t.strictEqual(stderr, '', 'default max-http-header-size should not throw') + }) + + await t.completed + }) }) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index d8ff177504c..1378de82cf5 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -16,7 +16,7 @@ describe('prioritize socket errors over timeouts', () => { client.request({ method: 'GET', path: '/foobar' }) .then(() => t.fail()) .catch((err) => { - t.strictEqual(err.code, 'ENOTFOUND') + t.strictEqual(['ENOTFOUND', 'EAI_AGAIN'].includes(err.code), true) }) // block for 1s which is enough for the dns lookup to complete and TO to fire diff --git a/test/issue-1670.js b/test/issue-1670.js index 7a0cda32669..26904d4da8f 100644 --- a/test/issue-1670.js +++ b/test/issue-1670.js @@ -3,7 +3,7 @@ const { test } = require('node:test') const { request } = require('..') -test('https://github.com/mcollina/undici/issues/810', async () => { +test('https://github.com/mcollina/undici/issues/1670', async () => { const { body } = await request('https://api.github.com/user/emails') await body.text() From dfe7bbd1265d05851539488f07dd93d8c29591c4 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 20 Feb 2024 09:57:33 +0100 Subject: [PATCH 044/123] mock: improve validateReplyParameters (#2783) --- lib/mock/mock-interceptor.js | 2 +- test/mock-interceptor.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mock/mock-interceptor.js b/lib/mock/mock-interceptor.js index 781e477502a..1772f0e424f 100644 --- a/lib/mock/mock-interceptor.js +++ b/lib/mock/mock-interceptor.js @@ -106,7 +106,7 @@ class MockInterceptor { if (typeof data === 'undefined') { throw new InvalidArgumentError('data must be defined') } - if (typeof responseOptions !== 'object') { + if (typeof responseOptions !== 'object' || responseOptions === null) { throw new InvalidArgumentError('responseOptions must be an object') } } diff --git a/test/mock-interceptor.js b/test/mock-interceptor.js index 036ea69df98..961a7576dc1 100644 --- a/test/mock-interceptor.js +++ b/test/mock-interceptor.js @@ -53,7 +53,7 @@ describe('MockInterceptor - reply callback', () => { }) test('should error if passed options invalid', t => { - t = tspl(t, { plan: 2 }) + t = tspl(t, { plan: 3 }) const mockInterceptor = new MockInterceptor({ path: '', @@ -61,6 +61,7 @@ describe('MockInterceptor - reply callback', () => { }, []) t.throws(() => mockInterceptor.reply(), new InvalidArgumentError('statusCode must be defined')) t.throws(() => mockInterceptor.reply(200, () => { }, 'hello'), new InvalidArgumentError('responseOptions must be an object')) + t.throws(() => mockInterceptor.reply(200, () => { }, null), new InvalidArgumentError('responseOptions must be an object')) }) }) From 78934dbcd12cbb3c2bbcc08c09b8e9bb8492cff9 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 20 Feb 2024 09:59:14 +0100 Subject: [PATCH 045/123] perf: improve TernarySearchTree (#2782) Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- benchmarks/TernarySearchTree.mjs | 20 ++++++++++++++++++++ lib/core/tree.js | 6 +++++- test/node-test/tree.js | 7 +++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 benchmarks/TernarySearchTree.mjs diff --git a/benchmarks/TernarySearchTree.mjs b/benchmarks/TernarySearchTree.mjs new file mode 100644 index 00000000000..413288a6af3 --- /dev/null +++ b/benchmarks/TernarySearchTree.mjs @@ -0,0 +1,20 @@ +import { bench, group, run } from 'mitata' +import { tree } from '../lib/core/tree.js' + +const contentLength = Buffer.from('Content-Length') +const contentLengthUpperCase = Buffer.from('Content-Length'.toUpperCase()) +const contentLengthLowerCase = Buffer.from('Content-Length'.toLowerCase()) + +group('tree.search', () => { + bench('content-length', () => { + tree.lookup(contentLengthLowerCase) + }) + bench('CONTENT-LENGTH', () => { + tree.lookup(contentLengthUpperCase) + }) + bench('Content-Length', () => { + tree.lookup(contentLength) + }) +}) + +await run() diff --git a/lib/core/tree.js b/lib/core/tree.js index 366fc7d3207..9b50767c6d3 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -83,7 +83,10 @@ class TstNode { while (node !== null && index < keylength) { let code = key[index] // A-Z - if (code >= 0x41 && code <= 0x5a) { + // First check if it is bigger than 0x5a. + // Lowercase letters have higher char codes than uppercase ones. + // Also we assume that headers will mostly contain lowercase characters. + if (code <= 0x5a && code >= 0x41) { // Lowercase for uppercase. code |= 32 } @@ -121,6 +124,7 @@ class TernarySearchTree { /** * @param {Uint8Array} key + * @return {any} */ lookup (key) { return this.node?.search(key)?.value ?? null diff --git a/test/node-test/tree.js b/test/node-test/tree.js index eee3fa85eac..44a7d7960ac 100644 --- a/test/node-test/tree.js +++ b/test/node-test/tree.js @@ -13,6 +13,13 @@ describe('Ternary Search Tree', () => { assert.throws(() => tst.insert(Buffer.from(''), '')) }) + test('looking up not inserted key returns null', () => { + assert.throws(() => new TernarySearchTree().insert(Buffer.from(''), '')) + const tst = new TernarySearchTree() + tst.insert(Buffer.from('a'), 'a') + assert.strictEqual(tst.lookup(Buffer.from('non-existant')), null) + }) + test('duplicate key', () => { const tst = new TernarySearchTree() const key = Buffer.from('a') From fd66cf04b222ee365cdca780d9b465f531f74eb5 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 20 Feb 2024 06:12:16 -0500 Subject: [PATCH 046/123] fix: convert HeadersInit to sequence/dictionary correctly (#2784) --- lib/fetch/headers.js | 6 ++++-- lib/fetch/webidl.js | 8 ++++---- test/fetch/headers.js | 27 +++++++++++++++++++++++++++ types/webidl.d.ts | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 43860c5d98a..41ae9b02368 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -528,8 +528,10 @@ Object.defineProperties(Headers.prototype, { webidl.converters.HeadersInit = function (V) { if (webidl.util.Type(V) === 'Object') { - if (V[Symbol.iterator]) { - return webidl.converters['sequence>'](V) + const iterator = Reflect.get(V, Symbol.iterator) + + if (typeof iterator === 'function') { + return webidl.converters['sequence>'](V, iterator.bind(V)) } return webidl.converters['record'](V) diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index a93a25505fc..0978e67f26f 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -218,7 +218,7 @@ webidl.util.IntegerPart = function (n) { // https://webidl.spec.whatwg.org/#es-sequence webidl.sequenceConverter = function (converter) { - return (V) => { + return (V, Iterable) => { // 1. If Type(V) is not Object, throw a TypeError. if (webidl.util.Type(V) !== 'Object') { throw webidl.errors.exception({ @@ -229,7 +229,7 @@ webidl.sequenceConverter = function (converter) { // 2. Let method be ? GetMethod(V, @@iterator). /** @type {Generator} */ - const method = V?.[Symbol.iterator]?.() + const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.() const seq = [] // 3. If method is undefined, throw a TypeError. @@ -273,8 +273,8 @@ webidl.recordConverter = function (keyConverter, valueConverter) { const result = {} if (!types.isProxy(O)) { - // Object.keys only returns enumerable properties - const keys = Object.keys(O) + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)] for (const key of keys) { // 1. Let typedKey be key converted to an IDL value of type K. diff --git a/test/fetch/headers.js b/test/fetch/headers.js index fcdf4b7a820..d3e27fa348d 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -719,3 +719,30 @@ test('When the value is updated, update the cache', (t) => { headers.append('d', 'd') deepStrictEqual([...headers], [...expected, ['d', 'd']]) }) + +test('Symbol.iterator is only accessed once', (t) => { + const { ok } = tspl(t, { plan: 1 }) + + const dict = new Proxy({}, { + get () { + ok(true) + + return function * () {} + } + }) + + new Headers(dict) // eslint-disable-line no-new +}) + +test('Invalid Symbol.iterators', (t) => { + const { throws } = tspl(t, { plan: 3 }) + + throws(() => new Headers({ [Symbol.iterator]: null }), TypeError) + throws(() => new Headers({ [Symbol.iterator]: undefined }), TypeError) + throws(() => { + const obj = { [Symbol.iterator]: null } + Object.defineProperty(obj, Symbol.iterator, { enumerable: false }) + + new Headers(obj) // eslint-disable-line no-new + }, TypeError) +}) diff --git a/types/webidl.d.ts b/types/webidl.d.ts index 40cfe064f8f..f29bebbb1e8 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -5,7 +5,7 @@ */ type Converter = (object: unknown) => T -type SequenceConverter = (object: unknown) => T[] +type SequenceConverter = (object: unknown, iterable?: IterableIterator) => T[] type RecordConverter = (object: unknown) => Record From 02107fee236c4310b5243e242a4d5662ede47e87 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 20 Feb 2024 19:05:11 +0100 Subject: [PATCH 047/123] chore: improve getFieldValue (#2785) * chore: improve getFieldValue * remove * Update benchmarks/cacheGetFieldValues.mjs --- benchmarks/cacheGetFieldValues.mjs | 23 +++++++++++++++++++++++ lib/cache/cache.js | 2 +- lib/cache/util.js | 12 ++++-------- lib/fetch/util.js | 4 +--- test/cache/get-field-values.js | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 benchmarks/cacheGetFieldValues.mjs create mode 100644 test/cache/get-field-values.js diff --git a/benchmarks/cacheGetFieldValues.mjs b/benchmarks/cacheGetFieldValues.mjs new file mode 100644 index 00000000000..50fd3185184 --- /dev/null +++ b/benchmarks/cacheGetFieldValues.mjs @@ -0,0 +1,23 @@ +import { bench, group, run } from 'mitata' +import { getFieldValues } from '../lib/cache/util.js' + +const values = [ + '', + 'foo', + 'invälid', + 'foo, ', + 'foo, bar', + 'foo, bar, baz', + 'foo, bar, baz, ', + 'foo, bar, baz, , ' +] + +group('getFieldValues', () => { + bench('getFieldValues', () => { + for (let i = 0; i < values.length; ++i) { + getFieldValues(values[i]) + } + }) +}) + +await run() diff --git a/lib/cache/cache.js b/lib/cache/cache.js index d49393d15bc..74cd802de7f 100644 --- a/lib/cache/cache.js +++ b/lib/cache/cache.js @@ -1,7 +1,7 @@ 'use strict' const { kConstruct } = require('./symbols') -const { urlEquals, fieldValues: getFieldValues } = require('./util') +const { urlEquals, getFieldValues } = require('./util') const { kEnumerableProperty, isDisturbed } = require('../core/util') const { webidl } = require('../fetch/webidl') const { Response, cloneResponse, fromInnerResponse } = require('../fetch/response') diff --git a/lib/cache/util.js b/lib/cache/util.js index eba8a60efa3..d168d45351b 100644 --- a/lib/cache/util.js +++ b/lib/cache/util.js @@ -23,7 +23,7 @@ function urlEquals (A, B, excludeFragment = false) { * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 * @param {string} header */ -function fieldValues (header) { +function getFieldValues (header) { assert(header !== null) const values = [] @@ -31,13 +31,9 @@ function fieldValues (header) { for (let value of header.split(',')) { value = value.trim() - if (!value.length) { - continue - } else if (!isValidHeaderName(value)) { - continue + if (isValidHeaderName(value)) { + values.push(value) } - - values.push(value) } return values @@ -45,5 +41,5 @@ function fieldValues (header) { module.exports = { urlEquals, - fieldValues + getFieldValues } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index fdda578213e..c07db2338a4 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -111,9 +111,7 @@ function isValidReasonPhrase (statusText) { * @see https://fetch.spec.whatwg.org/#header-name * @param {string} potentialValue */ -function isValidHeaderName (potentialValue) { - return isValidHTTPToken(potentialValue) -} +const isValidHeaderName = isValidHTTPToken /** * @see https://fetch.spec.whatwg.org/#header-value diff --git a/test/cache/get-field-values.js b/test/cache/get-field-values.js new file mode 100644 index 00000000000..7a1a91d523a --- /dev/null +++ b/test/cache/get-field-values.js @@ -0,0 +1,19 @@ +'use strict' + +const { deepStrictEqual, throws } = require('node:assert') +const { test } = require('node:test') +const { getFieldValues } = require('../../lib/cache/util') + +test('getFieldValues', () => { + throws(() => getFieldValues(null), { + name: 'AssertionError', + message: 'The expression evaluated to a falsy value:\n\n assert(header !== null)\n' + }) + deepStrictEqual(getFieldValues(''), []) + deepStrictEqual(getFieldValues('foo'), ['foo']) + deepStrictEqual(getFieldValues('invälid'), []) + deepStrictEqual(getFieldValues('foo, bar'), ['foo', 'bar']) + deepStrictEqual(getFieldValues('foo, bar, baz'), ['foo', 'bar', 'baz']) + deepStrictEqual(getFieldValues('foo, bar, baz, '), ['foo', 'bar', 'baz']) + deepStrictEqual(getFieldValues('foo, bar, baz, , '), ['foo', 'bar', 'baz']) +}) From 7936d69cb44426c0e7c24989005e875518f28dca Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 21 Feb 2024 11:40:11 +0100 Subject: [PATCH 048/123] Add RetryHandler to sidebar (#2797) * Add RetryHandler to sidebar * fixup Signed-off-by: Matteo Collina --------- Signed-off-by: Matteo Collina --- docsify/sidebar.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docsify/sidebar.md b/docsify/sidebar.md index e187c3080f6..0a0fb04e177 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -25,6 +25,7 @@ * [CacheStorage](/docs/api/CacheStorage.md "Undici API - CacheStorage") * [Util](/docs/api/Util.md "Undici API - Util") * [RedirectHandler](/docs/api/RedirectHandler.md "Undici API - RedirectHandler") + * [RetryHandler](/docs/api/RetryHandler.md "Undici API - RetryHandler") * Best Practices * [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy") * [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate") From d49304e576647895c657b0c8f355337cae457974 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 21 Feb 2024 12:30:40 +0100 Subject: [PATCH 049/123] Add RetryAgent (#2798) Signed-off-by: Matteo Collina --- docs/api/RetryAgent.md | 45 +++++++++++++++++++++ docs/api/RetryHandler.md | 2 +- docsify/sidebar.md | 1 + index.js | 2 + lib/handler/RetryHandler.js | 4 +- lib/retry-agent.js | 35 +++++++++++++++++ test/retry-agent.js | 67 ++++++++++++++++++++++++++++++++ test/types/retry-agent.test-d.ts | 17 ++++++++ types/index.d.ts | 3 +- types/retry-agent.d.ts | 11 ++++++ 10 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 docs/api/RetryAgent.md create mode 100644 lib/retry-agent.js create mode 100644 test/retry-agent.js create mode 100644 test/types/retry-agent.test-d.ts create mode 100644 types/retry-agent.d.ts diff --git a/docs/api/RetryAgent.md b/docs/api/RetryAgent.md new file mode 100644 index 00000000000..a1f38b3adb8 --- /dev/null +++ b/docs/api/RetryAgent.md @@ -0,0 +1,45 @@ +# Class: RetryAgent + +Extends: `undici.Dispatcher` + +A `undici.Dispatcher` that allows to automatically retry a request. +Wraps a `undici.RetryHandler`. + +## `new RetryAgent(dispatcher, [options])` + +Arguments: + +* **dispatcher** `undici.Dispatcher` (required) - the dispactgher to wrap +* **options** `RetryHandlerOptions` (optional) - the options + +Returns: `ProxyAgent` + +### Parameter: `RetryHandlerOptions` + +- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed. +- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5` +- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds) +- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second) +- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2` +- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true` +- +- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` +- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']` + +**`RetryContext`** + +- `state`: `RetryState` - Current retry state. It can be mutated. +- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler. + +Example: + +```js +import { Agent, RetryAgent } from 'undici' + +const agent = new RetryAgent(new Agent()) + +const res = await agent.request('http://example.com') +console.log(res.statuCode) +console.log(await res.body.text()) +``` diff --git a/docs/api/RetryHandler.md b/docs/api/RetryHandler.md index 2323ce47911..6a932da8bdc 100644 --- a/docs/api/RetryHandler.md +++ b/docs/api/RetryHandler.md @@ -28,7 +28,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions). - - **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` -- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']` **`RetryContext`** diff --git a/docsify/sidebar.md b/docsify/sidebar.md index 0a0fb04e177..674c0dad0e7 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -8,6 +8,7 @@ * [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool") * [Agent](/docs/api/Agent.md "Undici API - Agent") * [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent") + * [RetryAgent](/docs/api/RetryAgent.md "Undici API - RetryAgent") * [Connector](/docs/api/Connector.md "Custom connector") * [Errors](/docs/api/Errors.md "Undici API - Errors") * [EventSource](/docs/api/EventSource.md "Undici API - EventSource") diff --git a/index.js b/index.js index 2e274b24029..b3ad271653d 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent') const MockPool = require('./lib/mock/mock-pool') const mockErrors = require('./lib/mock/mock-errors') const ProxyAgent = require('./lib/proxy-agent') +const RetryAgent = require('./lib/retry-agent') const RetryHandler = require('./lib/handler/RetryHandler') const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') const DecoratorHandler = require('./lib/handler/DecoratorHandler') @@ -29,6 +30,7 @@ module.exports.Pool = Pool module.exports.BalancedPool = BalancedPool module.exports.Agent = Agent module.exports.ProxyAgent = ProxyAgent +module.exports.RetryAgent = RetryAgent module.exports.RetryHandler = RetryHandler module.exports.DecoratorHandler = DecoratorHandler diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js index 2a2f878c00f..f2e22f6d4c0 100644 --- a/lib/handler/RetryHandler.js +++ b/lib/handler/RetryHandler.js @@ -53,7 +53,8 @@ class RetryHandler { 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', - 'EPIPE' + 'EPIPE', + 'UND_ERR_SOCKET' ] } @@ -119,7 +120,6 @@ class RetryHandler { if ( code && code !== 'UND_ERR_REQ_RETRY' && - code !== 'UND_ERR_SOCKET' && !errorCodes.includes(code) ) { cb(err) diff --git a/lib/retry-agent.js b/lib/retry-agent.js new file mode 100644 index 00000000000..9edb2aa529f --- /dev/null +++ b/lib/retry-agent.js @@ -0,0 +1,35 @@ +'use strict' + +const Dispatcher = require('./dispatcher') +const RetryHandler = require('./handler/RetryHandler') + +class RetryAgent extends Dispatcher { + #agent = null + #options = null + constructor (agent, options = {}) { + super(options) + this.#agent = agent + this.#options = options + } + + dispatch (opts, handler) { + const retry = new RetryHandler({ + ...opts, + retryOptions: this.#options + }, { + dispatch: this.#agent.dispatch.bind(this.#agent), + handler + }) + return this.#agent.dispatch(opts, retry) + } + + close () { + return this.#agent.close() + } + + destroy () { + return this.#agent.destroy() + } +} + +module.exports = RetryAgent diff --git a/test/retry-agent.js b/test/retry-agent.js new file mode 100644 index 00000000000..0e5e252d954 --- /dev/null +++ b/test/retry-agent.js @@ -0,0 +1,67 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { createServer } = require('node:http') +const { once } = require('node:events') + +const { RetryAgent, Client } = require('..') +test('Should retry status code', async t => { + t = tspl(t, { plan: 2 }) + + let counter = 0 + const server = createServer() + const opts = { + maxRetries: 5, + timeout: 1, + timeoutFactor: 1 + } + + server.on('request', (req, res) => { + switch (counter++) { + case 0: + req.destroy() + return + case 1: + res.writeHead(500) + res.end('failed') + return + case 2: + res.writeHead(200) + res.end('hello world!') + return + default: + t.fail() + } + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const agent = new RetryAgent(client, opts) + + after(async () => { + await agent.close() + server.close() + + await once(server, 'close') + }) + + agent.request({ + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }).then((res) => { + t.equal(res.statusCode, 200) + res.body.setEncoding('utf8') + let chunks = '' + res.body.on('data', chunk => { chunks += chunk }) + res.body.on('end', () => { + t.equal(chunks, 'hello world!') + }) + }) + }) + + await t.completed +}) diff --git a/test/types/retry-agent.test-d.ts b/test/types/retry-agent.test-d.ts new file mode 100644 index 00000000000..9177efd07ff --- /dev/null +++ b/test/types/retry-agent.test-d.ts @@ -0,0 +1,17 @@ +import { expectAssignable } from 'tsd' +import { RetryAgent, Agent } from '../..' + +const dispatcher = new Agent() + +expectAssignable(new RetryAgent(dispatcher)) +expectAssignable(new RetryAgent(dispatcher, { maxRetries: 5 })) + +{ + const retryAgent = new RetryAgent(dispatcher) + + // close + expectAssignable>(retryAgent.close()) + + // dispatch + expectAssignable(retryAgent.dispatch({ origin: '', path: '', method: 'GET' }, {})) +} diff --git a/types/index.d.ts b/types/index.d.ts index 63e5c32bcef..05f01ea1f4d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -15,6 +15,7 @@ import MockAgent from'./mock-agent' import mockErrors from'./mock-errors' import ProxyAgent from'./proxy-agent' import RetryHandler from'./retry-handler' +import RetryAgent from'./retry-agent' import { request, pipeline, stream, connect, upgrade } from './api' export * from './util' @@ -30,7 +31,7 @@ export * from './content-type' export * from './cache' export { Interceptable } from './mock-interceptor' -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler } +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } export default Undici declare namespace Undici { diff --git a/types/retry-agent.d.ts b/types/retry-agent.d.ts new file mode 100644 index 00000000000..cf559d956e5 --- /dev/null +++ b/types/retry-agent.d.ts @@ -0,0 +1,11 @@ +import Agent from './agent' +import buildConnector from './connector'; +import Dispatcher from './dispatcher' +import { IncomingHttpHeaders } from './header' +import RetryHandler from './retry-handler' + +export default RetryAgent + +declare class RetryAgent extends Dispatcher { + constructor(dispatcher: Dispatcher, options?: RetryHandler.RetryOptions) +} From 2297e6979d3d9589ab366fc84b9740cc29599a91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:42:29 +0000 Subject: [PATCH 050/123] build(deps): bump step-security/harden-runner from 2.6.0 to 2.7.0 (#2690) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.6.0 to 2.7.0. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/1b05615854632b887b69ae1be8cbefe72d3ae423...63c24ba6bd7ba022e95695ff85de572c04a18142) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e856b98b148..2bf2eaf85c5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1a55ae4ddce..9266eef6689 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit From f0c1a7a6681074a3d40e5b66295fd3a89699f5ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:58:20 +0000 Subject: [PATCH 051/123] build(deps): bump actions/checkout from 4.1.0 to 4.1.1 (#2393) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8ade135a41bc03ea155e62e844d188df1ea18608...b4ffde65f46336ab88eb53be808477a3936bae11) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/publish-undici-types.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 2a334258857..9cf58db98bd 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false ref: ${{ github.base_ref }} @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: Setup Node diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2bf2eaf85c5..ef05b49ee0d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 9266eef6689..74f249a0c77 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 42eced4bc7a..45265c6872c 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 80dea4e3163..49fd98ea6ad 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index 91687ca8717..efd62556aa0 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -13,7 +13,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: '16.x' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 75af3b5aca4..3a146eade6e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false From 724d064bb4a59c5bcf79bdd2e9ead7665e780706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:18:33 +0100 Subject: [PATCH 052/123] build(deps): bump actions/upload-artifact from 3.1.3 to 4.3.1 (#2799) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.3 to 4.3.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/a8a3f3ad30e3422c9c7b888a15615d19a852ae32...5d5d22a31266ced268874388b861e4b58bb5c2f3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fuzz.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 45265c6872c..87975c98338 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -29,7 +29,7 @@ jobs: run: | npm run fuzz - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 if: ${{ failure() }} with: name: undici-fuzz-results-${{ github.sha }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3a146eade6e..c92e564fa4f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: SARIF file path: results.sarif From bdfb86327ba898f6ea1147b48917ee26f967f476 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:26:42 +0000 Subject: [PATCH 053/123] build(deps): bump node from 20-alpine to 21-alpine in /build (#2803) Bumps node from 20-alpine to 21-alpine. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 5438b73690c..f42e88bac30 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine@sha256:4559bc033338938e54d0a3c2f0d7c3ad7d1d13c28c4c405b85c6b3a26f4ce5f7 +FROM node:21-alpine@sha256:d3271e4bd89eec4d97087060fd4db0c238d9d22fcfad090a73fa9b5128699888 ARG UID=1000 ARG GID=1000 From fdbc2219514575665f6bcf812ac913a3c36e6f8d Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 23:40:37 +0900 Subject: [PATCH 054/123] perf: improve sort algorithm (#2756) * perf: improve sort algorithm * benchmark: add headers-length32.mjs * fix: benchmark * fix: fix performance regression for sorted arrays * test: add sorted test * refactor: simplify * refactor: remove comment --- benchmarks/headers-length32.mjs | 56 ++++++++ benchmarks/headers.mjs | 57 ++++++++ benchmarks/sort.mjs | 50 +++++++ lib/fetch/headers.js | 114 +++++++++++++--- lib/fetch/sort.js | 187 ++++++++++++++++++++++++++ package.json | 2 +- test/fetch/headerslist-sortedarray.js | 38 ++++++ test/fetch/sort.js | 90 +++++++++++++ 8 files changed, 571 insertions(+), 23 deletions(-) create mode 100644 benchmarks/headers-length32.mjs create mode 100644 benchmarks/headers.mjs create mode 100644 benchmarks/sort.mjs create mode 100644 lib/fetch/sort.js create mode 100644 test/fetch/headerslist-sortedarray.js create mode 100644 test/fetch/sort.js diff --git a/benchmarks/headers-length32.mjs b/benchmarks/headers-length32.mjs new file mode 100644 index 00000000000..d057ed77eeb --- /dev/null +++ b/benchmarks/headers-length32.mjs @@ -0,0 +1,56 @@ +import { bench, run } from 'mitata' +import { Headers } from '../lib/fetch/headers.js' + +const headers = new Headers( + [ + 'Origin-Agent-Cluster', + 'RTT', + 'Accept-CH-Lifetime', + 'X-Frame-Options', + 'Sec-CH-UA-Platform-Version', + 'Digest', + 'Cache-Control', + 'Sec-CH-UA-Platform', + 'If-Range', + 'SourceMap', + 'Strict-Transport-Security', + 'Want-Digest', + 'Cross-Origin-Resource-Policy', + 'Width', + 'Accept-CH', + 'Via', + 'Refresh', + 'Server', + 'Sec-Fetch-Dest', + 'Sec-CH-UA-Model', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'Date', + 'Expires', + 'DNT', + 'Proxy-Authorization', + 'Alt-Svc', + 'Alt-Used', + 'ETag', + 'Sec-Fetch-User', + 'Sec-CH-UA-Full-Version-List', + 'Referrer-Policy' + ].map((v) => [v, '']) +) + +const kHeadersList = Reflect.ownKeys(headers).find( + (c) => String(c) === 'Symbol(headers list)' +) + +const headersList = headers[kHeadersList] + +const kHeadersSortedMap = Reflect.ownKeys(headersList).find( + (c) => String(c) === 'Symbol(headers map sorted)' +) + +bench('Headers@@iterator', () => { + headersList[kHeadersSortedMap] = null + return [...headers] +}) + +await run() diff --git a/benchmarks/headers.mjs b/benchmarks/headers.mjs new file mode 100644 index 00000000000..0484e6e1316 --- /dev/null +++ b/benchmarks/headers.mjs @@ -0,0 +1,57 @@ +import { bench, group, run } from 'mitata' +import { Headers } from '../lib/fetch/headers.js' + +const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' +const charactersLength = characters.length + +function generateAsciiString (length) { + let result = '' + for (let i = 0; i < length; ++i) { + result += characters[Math.floor(Math.random() * charactersLength)] + } + return result +} + +const settings = { + 'fast-path (tiny array)': 4, + 'fast-path (small array)': 8, + 'fast-path (middle array)': 16, + 'fast-path': 32, + 'slow-path': 64 +} + +for (const [name, length] of Object.entries(settings)) { + const headers = new Headers( + Array.from(Array(length), () => [generateAsciiString(12), '']) + ) + + const headersSorted = new Headers(headers) + + const kHeadersList = Reflect.ownKeys(headers).find( + (c) => String(c) === 'Symbol(headers list)' + ) + + const headersList = headers[kHeadersList] + + const headersListSorted = headersSorted[kHeadersList] + + const kHeadersSortedMap = Reflect.ownKeys(headersList).find( + (c) => String(c) === 'Symbol(headers map sorted)' + ) + + group(`length ${length} #${name}`, () => { + bench('Headers@@iterator', () => { + // prevention of memoization of results + headersList[kHeadersSortedMap] = null + return [...headers] + }) + + bench('Headers@@iterator (sorted)', () => { + // prevention of memoization of results + headersListSorted[kHeadersSortedMap] = null + return [...headersSorted] + }) + }) +} + +await run() diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs new file mode 100644 index 00000000000..a1c413a00de --- /dev/null +++ b/benchmarks/sort.mjs @@ -0,0 +1,50 @@ +import { bench, group, run } from 'mitata' +import { sort, heapSort, introSort } from '../lib/fetch/sort.js' + +function compare (a, b) { + return a < b ? -1 : 1 +} + +const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' +const charactersLength = characters.length + +function generateAsciiString (length) { + let result = '' + for (let i = 0; i < length; ++i) { + result += characters[Math.floor(Math.random() * charactersLength)] + } + return result +} + +const settings = { + tiny: 32, + small: 64, + middle: 128, + large: 512 +} + +for (const [name, length] of Object.entries(settings)) { + group(`sort (${name})`, () => { + const array = Array.from(new Array(length), () => generateAsciiString(12)) + // sort(array, compare) + bench('Array#sort', () => array.slice().sort(compare)) + bench('sort (intro sort)', () => sort(array.slice(), compare)) + + // sort(array, start, end, compare) + bench('intro sort', () => introSort(array.slice(), 0, array.length, compare)) + bench('heap sort', () => heapSort(array.slice(), 0, array.length, compare)) + }) + + group(`sort sortedArray (${name})`, () => { + const array = Array.from(new Array(length), () => generateAsciiString(12)).sort(compare) + // sort(array, compare) + bench('Array#sort', () => array.sort(compare)) + bench('sort (intro sort)', () => sort(array, compare)) + + // sort(array, start, end, compare) + bench('intro sort', () => introSort(array, 0, array.length, compare)) + bench('heap sort', () => heapSort(array, 0, array.length, compare)) + }) +} + +await run() diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 41ae9b02368..8ee66fbf996 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -12,6 +12,7 @@ const { } = require('./util') const { webidl } = require('./webidl') const assert = require('node:assert') +const { sort } = require('./sort') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') @@ -120,6 +121,10 @@ function appendHeader (headers, name, value) { // privileged no-CORS request headers from headers } +function compareHeaderName (a, b) { + return a[0] < b[0] ? -1 : 1 +} + class HeadersList { /** @type {[string, string][]|null} */ cookies = null @@ -237,7 +242,7 @@ class HeadersList { * [Symbol.iterator] () { // use the lowercased name - for (const [name, { value }] of this[kHeadersMap]) { + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { yield [name, value] } } @@ -253,6 +258,79 @@ class HeadersList { return headers } + + // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set + toSortedArray () { + const size = this[kHeadersMap].size + const array = new Array(size) + // In most cases, you will use the fast-path. + // fast-path: Use binary insertion sort for small arrays. + if (size <= 32) { + if (size === 0) { + // If empty, it is an empty array. To avoid the first index assignment. + return array + } + // Improve performance by unrolling loop and avoiding double-loop. + // Double-loop-less version of the binary insertion sort. + const iterator = this[kHeadersMap][Symbol.iterator]() + const firstValue = iterator.next().value + // set [name, value] to first index. + array[0] = [firstValue[0], firstValue[1].value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(firstValue[1].value !== null) + for ( + let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value; + i < size; + ++i + ) { + // get next value + value = iterator.next().value + // set [name, value] to current index. + x = array[i] = [value[0], value[1].value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(x[1] !== null) + left = 0 + right = i + // binary search + while (left < right) { + // middle index + pivot = left + ((right - left) >> 1) + // compare header name + if (array[pivot][0] <= x[0]) { + left = pivot + 1 + } else { + right = pivot + } + } + if (i !== pivot) { + j = i + while (j > left) { + array[j] = array[--j] + } + array[left] = x + } + } + /* c8 ignore next 4 */ + if (!iterator.next().done) { + // This is for debugging and will never be called. + throw new TypeError('Unreachable') + } + return array + } else { + // This case would be a rare occurrence. + // slow-path: fallback + let i = 0 + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { + array[i++] = [name, value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(value !== null) + } + return sort(array, compareHeaderName) + } + } } // https://fetch.spec.whatwg.org/#headers-class @@ -454,27 +532,19 @@ class Headers { // 2. Let names be the result of convert header names to a sorted-lowercase // set with all the names of the headers in list. - const names = [...this[kHeadersList]] - const namesLength = names.length - if (namesLength <= 16) { - // Note: Use insertion sort for small arrays. - for (let i = 1, value, j = 0; i < namesLength; ++i) { - value = names[i] - for (j = i - 1; j >= 0; --j) { - if (names[j][0] <= value[0]) break - names[j + 1] = names[j] - } - names[j + 1] = value - } - } else { - names.sort((a, b) => a[0] < b[0] ? -1 : 1) - } + const names = this[kHeadersList].toSortedArray() const cookies = this[kHeadersList].cookies + // fast-path + if (cookies === null) { + // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray` + return (this[kHeadersList][kHeadersSortedMap] = names) + } + // 3. For each name of names: - for (let i = 0; i < namesLength; ++i) { - const [name, value] = names[i] + for (let i = 0; i < names.length; ++i) { + const { 0: name, 1: value } = names[i] // 1. If name is `set-cookie`, then: if (name === 'set-cookie') { // 1. Let values be a list of all values of headers in list whose name @@ -491,17 +561,15 @@ class Headers { // 1. Let value be the result of getting name from list. // 2. Assert: value is non-null. - assert(value !== null) + // Note: This operation was done by `HeadersList#toSortedArray`. // 3. Append (name, value) to headers. headers.push([name, value]) } } - this[kHeadersList][kHeadersSortedMap] = headers - // 4. Return headers. - return headers + return (this[kHeadersList][kHeadersSortedMap] = headers) } [Symbol.for('nodejs.util.inspect.custom')] () { @@ -546,6 +614,8 @@ webidl.converters.HeadersInit = function (V) { module.exports = { fill, + // for test. + compareHeaderName, Headers, HeadersList } diff --git a/lib/fetch/sort.js b/lib/fetch/sort.js new file mode 100644 index 00000000000..230f2e2645c --- /dev/null +++ b/lib/fetch/sort.js @@ -0,0 +1,187 @@ +'use strict' + +/** **binary insertion sort** + * - Best -> O(n) + * - Average -> O(n^2) + * - Worst -> O(n^2) + * - Memory -> O(n) total, O(1) auxiliary + * - Stable -> true + * @param {any[]} array + * @param {number} begin begin + * @param {number} end end + * @param {(a: any, b: any) => number} compare + */ +function binaryInsertionSort (array, begin, end, compare) { + for ( + let i = begin + 1, j = 0, right = 0, left = 0, pivot = 0, x; + i < end; + ++i + ) { + x = array[i] + left = 0 + right = i + // binary search + while (left < right) { + // middle index + pivot = left + ((right - left) >> 1) + if (compare(array[pivot], x) <= 0) { + left = pivot + 1 + } else { + right = pivot + } + } + if (i !== pivot) { + j = i + while (j > left) { + array[j] = array[--j] + } + array[left] = x + } + } + return array +} + +/** + * @param {number} num + */ +function log2 (num) { + // Math.floor(Math.log2(num)) + let log = 0 + // eslint-disable-next-line no-cond-assign + while ((num >>= 1)) ++log + return log +} + +/** **intro sort** + * - Average -> O(n log n) + * - Worst -> O(n log n) + * - Stable -> false + * @param {any[]} array + * @param {number} begin begin + * @param {number} end end + * @param {(a: any, b: any) => number} compare + */ +function introSort (array, begin, end, compare) { + return _introSort(array, begin, end, log2(end - begin) << 1, compare) +} + +/** + * @param {any[]} array + * @param {number} begin + * @param {number} end + * @param {number} depth + * @param {(a: any, b: any) => number} compare + */ +function _introSort (array, begin, end, depth, compare) { + if (end - begin <= 32) { + return binaryInsertionSort(array, begin, end, compare) + } + if (depth-- <= 0) { + return heapSort(array, begin, end, compare) + } + // median of three quick sort + let i = begin + let j = end - 1 + const pivot = medianOf3( + array[i], + array[i + ((j - i) >> 1)], + array[j], + compare + ) + let firstPass = true + while (true) { + while (compare(array[i], pivot) < 0) ++i + while (compare(pivot, array[j]) < 0) --j + if (i >= j) break; + [array[i], array[j]] = [array[j], array[i]] + ++i + --j + firstPass = false + } + if (i - begin > 1 && !firstPass) _introSort(array, begin, i, depth, compare) + // if (end - (j + 1) > 1) ... + if (end - j > 2 && !firstPass) _introSort(array, j + 1, end, depth, compare) + return array +} + +/** **heap sort (bottom up)** + * - Best -> Ω(n) + * - Average -> O(n log n) + * - Worst -> O(n log n) + * - Memory -> O(n) total, O(1) auxiliary + * - Stable -> false + * @param {any[]} array + * @param {number} begin + * @param {number} end + * @param {(a: any, b: any) => number} compare + */ +function heapSort (array, begin, end, compare) { + const N = end - begin + let p = N >> 1 + let q = N - 1 + let x + while (p > 0) { + downHeap(array, array[begin + p - 1], begin, --p, q, compare) + } + while (q > 0) { + x = array[begin + q] + array[begin + q] = array[begin] + downHeap(array, (array[begin] = x), begin, 0, --q, compare) + } + return array +} + +/** + * @param {any[]} array + * @param {any} x + * @param {number} begin + * @param {number} p + * @param {number} q + * @param {(a: any, b: any) => number} compare + */ +function downHeap (array, x, begin, p, q, compare) { + let c + while ((c = (p << 1) + 1) <= q) { + if (c < q && compare(array[begin + c], array[begin + c + 1]) < 0) ++c + if (compare(x, array[begin + c]) >= 0) break + array[begin + p] = array[begin + c] + p = c + } + array[begin + p] = x +} + +/** + * @param {any} x + * @param {any} y + * @param {any} z + * @param {(a: any, b: any) => number} compare + */ +function medianOf3 (x, y, z, compare) { + return compare(x, y) < 0 + ? compare(y, z) < 0 + ? y + : compare(z, x) < 0 + ? x + : z + : compare(z, y) < 0 + ? y + : compare(x, z) < 0 + ? x + : z +} + +/** + * @param {any[]} array + * @param {(a: any, b: any) => number} compare + */ +function sort (array, compare) { + const length = array.length + return _introSort(array, 0, length, log2(length) << 1, compare) +} + +module.exports = { + sort, + binaryInsertionSort, + introSort, + heapSort +} diff --git a/package.json b/package.json index c85ec55102f..fabb4d5f1ba 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "jest": "^29.0.2", "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", - "mitata": "^0.1.8", + "mitata": "^0.1.10", "node-fetch": "^3.3.2", "pre-commit": "^1.2.2", "proxy": "^1.0.2", diff --git a/test/fetch/headerslist-sortedarray.js b/test/fetch/headerslist-sortedarray.js new file mode 100644 index 00000000000..72112f92288 --- /dev/null +++ b/test/fetch/headerslist-sortedarray.js @@ -0,0 +1,38 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { HeadersList, compareHeaderName } = require('../../lib/fetch/headers') + +const characters = 'abcdefghijklmnopqrstuvwxyz0123456789' +const charactersLength = characters.length + +function generateAsciiString (length) { + let result = '' + for (let i = 0; i < length; ++i) { + result += characters[Math.floor(Math.random() * charactersLength)] + } + return result +} + +const SORT_RUN = 4000 + +test('toSortedArray (fast-path)', () => { + for (let i = 0; i < SORT_RUN; ++i) { + const headersList = new HeadersList() + for (let j = 0; j < 32; ++j) { + headersList.append(generateAsciiString(4), generateAsciiString(4)) + } + assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + } +}) + +test('toSortedArray (slow-path)', () => { + for (let i = 0; i < SORT_RUN; ++i) { + const headersList = new HeadersList() + for (let j = 0; j < 64; ++j) { + headersList.append(generateAsciiString(4), generateAsciiString(4)) + } + assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + } +}) diff --git a/test/fetch/sort.js b/test/fetch/sort.js new file mode 100644 index 00000000000..a373c2a62ef --- /dev/null +++ b/test/fetch/sort.js @@ -0,0 +1,90 @@ +'use strict' + +const { describe, test } = require('node:test') +const assert = require('node:assert') +const { sort, heapSort, binaryInsertionSort, introSort } = require('../../lib/fetch/sort') + +function generateRandomNumberArray (length) { + const array = new Uint16Array(length) + for (let i = 0; i < length; ++i) { + array[i] = (65535 * Math.random()) | 0 + } + return array +} + +const compare = (a, b) => a - b + +const SORT_RUN = 4000 + +const SORT_ELEMENT = 200 + +describe('sort', () => { + const arrays = new Array(SORT_RUN) + const expectedArrays = new Array(SORT_RUN) + + for (let i = 0; i < SORT_RUN; ++i) { + const array = generateRandomNumberArray(SORT_ELEMENT) + const expected = array.slice().sort(compare) + arrays[i] = array + expectedArrays[i] = expected + } + + test('binary insertion sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('heap sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('intro sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) + } + }) +}) + +describe('sorted', () => { + const arrays = new Array(SORT_RUN) + const expectedArrays = new Array(SORT_RUN) + + for (let i = 0; i < SORT_RUN; ++i) { + const array = generateRandomNumberArray(SORT_ELEMENT).sort(compare) + arrays[i] = array + expectedArrays[i] = array.slice() + } + + test('binary insertion sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('heap sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('intro sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) + } + }) +}) From 8db8d9d42ed65a7c83075bfd94abb16f5cc5034a Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 21 Feb 2024 17:51:05 +0100 Subject: [PATCH 055/123] refactor: move web stuff into their own folder (#2793) Refs: https://github.com/nodejs/undici/issues/2732 --- benchmarks/cacheGetFieldValues.mjs | 2 +- benchmarks/headers-length32.mjs | 2 +- benchmarks/headers.mjs | 2 +- benchmarks/sort.mjs | 2 +- index-fetch.js | 16 +++++----- index.js | 30 +++++++++---------- lib/cache/symbols.js | 5 ---- lib/client.js | 2 +- lib/mock/mock-interceptor.js | 2 +- lib/{ => web}/cache/cache.js | 4 +-- lib/{ => web}/cache/cachestorage.js | 2 +- lib/web/cache/symbols.js | 5 ++++ lib/{ => web}/cache/util.js | 0 lib/{ => web}/cookies/constants.js | 0 lib/{ => web}/cookies/index.js | 0 lib/{ => web}/cookies/parse.js | 0 lib/{ => web}/cookies/util.js | 2 +- .../eventsource/eventsource-stream.js | 0 lib/{ => web}/eventsource/eventsource.js | 2 +- lib/{ => web}/eventsource/util.js | 0 lib/{ => web}/fetch/LICENSE | 0 lib/{ => web}/fetch/body.js | 4 +-- lib/{ => web}/fetch/constants.js | 0 lib/{ => web}/fetch/dataURL.js | 0 .../fetch}/dispatcher-weakref.js | 2 +- lib/{ => web}/fetch/file.js | 2 +- lib/{ => web}/fetch/formdata.js | 2 +- lib/{ => web}/fetch/global.js | 0 lib/{ => web}/fetch/headers.js | 4 +-- lib/{ => web}/fetch/index.js | 4 +-- lib/{ => web}/fetch/request.js | 6 ++-- lib/{ => web}/fetch/response.js | 4 +-- lib/{ => web}/fetch/sort.js | 0 lib/{ => web}/fetch/symbols.js | 0 lib/{ => web}/fetch/util.js | 2 +- lib/{ => web}/fetch/webidl.js | 2 +- lib/{ => web}/fileapi/encoding.js | 0 lib/{ => web}/fileapi/filereader.js | 2 +- lib/{ => web}/fileapi/progressevent.js | 0 lib/{ => web}/fileapi/symbols.js | 0 lib/{ => web}/fileapi/util.js | 0 lib/{ => web}/websocket/connection.js | 6 ++-- lib/{ => web}/websocket/constants.js | 0 lib/{ => web}/websocket/events.js | 2 +- lib/{ => web}/websocket/frame.js | 0 lib/{ => web}/websocket/receiver.js | 2 +- lib/{ => web}/websocket/symbols.js | 0 lib/{ => web}/websocket/util.js | 0 lib/{ => web}/websocket/websocket.js | 4 +-- test/cache/get-field-values.js | 2 +- test/cookie/global-headers.js | 2 +- test/eventsource/eventsource-attributes.js | 2 +- test/eventsource/eventsource-close.js | 2 +- test/eventsource/eventsource-connect.js | 2 +- .../eventsource-constructor-stringify.js | 2 +- test/eventsource/eventsource-constructor.js | 2 +- test/eventsource/eventsource-message.js | 2 +- test/eventsource/eventsource-reconnect.js | 2 +- test/eventsource/eventsource-redirecting.js | 2 +- .../eventsource-request-status-error.js | 2 +- test/eventsource/eventsource-stream-bom.js | 2 +- .../eventsource-stream-parse-line.js | 2 +- .../eventsource-stream-process-event.js | 2 +- test/eventsource/eventsource-stream.js | 2 +- test/eventsource/eventsource.js | 2 +- test/eventsource/util.js | 2 +- test/fetch/data-uri.js | 2 +- test/fetch/file.js | 2 +- test/fetch/headers.js | 4 +-- test/fetch/headerslist-sortedarray.js | 2 +- test/fetch/request.js | 4 +-- test/fetch/response.js | 4 +-- test/fetch/sort.js | 2 +- test/fetch/util.js | 4 +-- test/jest/instanceof-error.test.js | 2 +- test/node-fetch/main.js | 6 ++-- test/node-fetch/response.js | 2 +- test/webidl/converters.js | 2 +- test/webidl/helpers.js | 2 +- test/webidl/util.js | 2 +- test/websocket/events.js | 2 +- test/websocket/frame.js | 4 +-- test/wpt/runner/worker.mjs | 12 ++++---- 83 files changed, 109 insertions(+), 109 deletions(-) delete mode 100644 lib/cache/symbols.js rename lib/{ => web}/cache/cache.js (99%) rename lib/{ => web}/cache/cachestorage.js (98%) create mode 100644 lib/web/cache/symbols.js rename lib/{ => web}/cache/util.js (100%) rename lib/{ => web}/cookies/constants.js (100%) rename lib/{ => web}/cookies/index.js (100%) rename lib/{ => web}/cookies/parse.js (100%) rename lib/{ => web}/cookies/util.js (99%) rename lib/{ => web}/eventsource/eventsource-stream.js (100%) rename lib/{ => web}/eventsource/eventsource.js (99%) rename lib/{ => web}/eventsource/util.js (100%) rename lib/{ => web}/fetch/LICENSE (100%) rename lib/{ => web}/fetch/body.js (99%) rename lib/{ => web}/fetch/constants.js (100%) rename lib/{ => web}/fetch/dataURL.js (100%) rename lib/{compat => web/fetch}/dispatcher-weakref.js (93%) rename lib/{ => web}/fetch/file.js (99%) rename lib/{ => web}/fetch/formdata.js (99%) rename lib/{ => web}/fetch/global.js (100%) rename lib/{ => web}/fetch/headers.js (99%) rename lib/{ => web}/fetch/index.js (99%) rename lib/{ => web}/fetch/request.js (99%) rename lib/{ => web}/fetch/response.js (99%) rename lib/{ => web}/fetch/sort.js (100%) rename lib/{ => web}/fetch/symbols.js (100%) rename lib/{ => web}/fetch/util.js (99%) rename lib/{ => web}/fetch/webidl.js (99%) rename lib/{ => web}/fileapi/encoding.js (100%) rename lib/{ => web}/fileapi/filereader.js (99%) rename lib/{ => web}/fileapi/progressevent.js (100%) rename lib/{ => web}/fileapi/symbols.js (100%) rename lib/{ => web}/fileapi/util.js (100%) rename lib/{ => web}/websocket/connection.js (98%) rename lib/{ => web}/websocket/constants.js (100%) rename lib/{ => web}/websocket/events.js (99%) rename lib/{ => web}/websocket/frame.js (100%) rename lib/{ => web}/websocket/receiver.js (99%) rename lib/{ => web}/websocket/symbols.js (100%) rename lib/{ => web}/websocket/util.js (100%) rename lib/{ => web}/websocket/websocket.js (99%) diff --git a/benchmarks/cacheGetFieldValues.mjs b/benchmarks/cacheGetFieldValues.mjs index 50fd3185184..9a685d24544 100644 --- a/benchmarks/cacheGetFieldValues.mjs +++ b/benchmarks/cacheGetFieldValues.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { getFieldValues } from '../lib/cache/util.js' +import { getFieldValues } from '../lib/web/cache/util.js' const values = [ '', diff --git a/benchmarks/headers-length32.mjs b/benchmarks/headers-length32.mjs index d057ed77eeb..9f1c0a906f8 100644 --- a/benchmarks/headers-length32.mjs +++ b/benchmarks/headers-length32.mjs @@ -1,5 +1,5 @@ import { bench, run } from 'mitata' -import { Headers } from '../lib/fetch/headers.js' +import { Headers } from '../lib/web/fetch/headers.js' const headers = new Headers( [ diff --git a/benchmarks/headers.mjs b/benchmarks/headers.mjs index 0484e6e1316..8722cf11c1f 100644 --- a/benchmarks/headers.mjs +++ b/benchmarks/headers.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { Headers } from '../lib/fetch/headers.js' +import { Headers } from '../lib/web/fetch/headers.js' const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' const charactersLength = characters.length diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs index a1c413a00de..bc5d2bc71ed 100644 --- a/benchmarks/sort.mjs +++ b/benchmarks/sort.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { sort, heapSort, introSort } from '../lib/fetch/sort.js' +import { sort, heapSort, introSort } from '../lib/web/fetch/sort.js' function compare (a, b) { return a < b ? -1 : 1 diff --git a/index-fetch.js b/index-fetch.js index 851731865b5..b8b3f3c7cac 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -1,6 +1,6 @@ 'use strict' -const fetchImpl = require('./lib/fetch').fetch +const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = function fetch (resource, init = undefined) { return fetchImpl(resource, init).catch((err) => { @@ -10,12 +10,12 @@ module.exports.fetch = function fetch (resource, init = undefined) { throw err }) } -module.exports.FormData = require('./lib/fetch/formdata').FormData -module.exports.Headers = require('./lib/fetch/headers').Headers -module.exports.Response = require('./lib/fetch/response').Response -module.exports.Request = require('./lib/fetch/request').Request +module.exports.FormData = require('./lib/web/fetch/formdata').FormData +module.exports.Headers = require('./lib/web/fetch/headers').Headers +module.exports.Response = require('./lib/web/fetch/response').Response +module.exports.Request = require('./lib/web/fetch/request').Request -module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket -module.exports.MessageEvent = require('./lib/websocket/events').MessageEvent +module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket +module.exports.MessageEvent = require('./lib/web/websocket/events').MessageEvent -module.exports.EventSource = require('./lib/eventsource/eventsource').EventSource +module.exports.EventSource = require('./lib/web/eventsource/eventsource').EventSource diff --git a/index.js b/index.js index b3ad271653d..dd52f2e46a1 100644 --- a/index.js +++ b/index.js @@ -96,7 +96,7 @@ function makeDispatcher (fn) { module.exports.setGlobalDispatcher = setGlobalDispatcher module.exports.getGlobalDispatcher = getGlobalDispatcher -const fetchImpl = require('./lib/fetch').fetch +const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = async function fetch (init, options = undefined) { try { return await fetchImpl(init, options) @@ -108,39 +108,39 @@ module.exports.fetch = async function fetch (init, options = undefined) { throw err } } -module.exports.Headers = require('./lib/fetch/headers').Headers -module.exports.Response = require('./lib/fetch/response').Response -module.exports.Request = require('./lib/fetch/request').Request -module.exports.FormData = require('./lib/fetch/formdata').FormData -module.exports.File = require('./lib/fetch/file').File -module.exports.FileReader = require('./lib/fileapi/filereader').FileReader +module.exports.Headers = require('./lib/web/fetch/headers').Headers +module.exports.Response = require('./lib/web/fetch/response').Response +module.exports.Request = require('./lib/web/fetch/request').Request +module.exports.FormData = require('./lib/web/fetch/formdata').FormData +module.exports.File = require('./lib/web/fetch/file').File +module.exports.FileReader = require('./lib/web/fileapi/filereader').FileReader -const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global') +const { setGlobalOrigin, getGlobalOrigin } = require('./lib/web/fetch/global') module.exports.setGlobalOrigin = setGlobalOrigin module.exports.getGlobalOrigin = getGlobalOrigin -const { CacheStorage } = require('./lib/cache/cachestorage') -const { kConstruct } = require('./lib/cache/symbols') +const { CacheStorage } = require('./lib/web/cache/cachestorage') +const { kConstruct } = require('./lib/web/cache/symbols') // Cache & CacheStorage are tightly coupled with fetch. Even if it may run // in an older version of Node, it doesn't have any use without fetch. module.exports.caches = new CacheStorage(kConstruct) -const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies') +const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/web/cookies') module.exports.deleteCookie = deleteCookie module.exports.getCookies = getCookies module.exports.getSetCookies = getSetCookies module.exports.setCookie = setCookie -const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL') +const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/dataURL') module.exports.parseMIMEType = parseMIMEType module.exports.serializeAMimeType = serializeAMimeType -const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/websocket/events') -module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket +const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events') +module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket module.exports.CloseEvent = CloseEvent module.exports.ErrorEvent = ErrorEvent module.exports.MessageEvent = MessageEvent @@ -156,6 +156,6 @@ module.exports.MockPool = MockPool module.exports.MockAgent = MockAgent module.exports.mockErrors = mockErrors -const { EventSource } = require('./lib/eventsource/eventsource') +const { EventSource } = require('./lib/web/eventsource/eventsource') module.exports.EventSource = EventSource diff --git a/lib/cache/symbols.js b/lib/cache/symbols.js deleted file mode 100644 index 40448d6001e..00000000000 --- a/lib/cache/symbols.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = { - kConstruct: require('../core/symbols').kConstruct -} diff --git a/lib/client.js b/lib/client.js index b74ffcbf7be..7a7260406ef 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1500,7 +1500,7 @@ function write (client, request) { if (util.isFormDataLike(body)) { if (!extractBody) { - extractBody = require('./fetch/body.js').extractBody + extractBody = require('./web/fetch/body.js').extractBody } const [bodyStream, contentType] = extractBody(body) diff --git a/lib/mock/mock-interceptor.js b/lib/mock/mock-interceptor.js index 1772f0e424f..d071a90db06 100644 --- a/lib/mock/mock-interceptor.js +++ b/lib/mock/mock-interceptor.js @@ -74,7 +74,7 @@ class MockInterceptor { if (opts.query) { opts.path = buildURL(opts.path, opts.query) } else { - // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + // Matches https://github.com/nodejs/undici/blob/main/lib/web/fetch/index.js#L1811 const parsedURL = new URL(opts.path, 'data://') opts.path = parsedURL.pathname + parsedURL.search } diff --git a/lib/cache/cache.js b/lib/web/cache/cache.js similarity index 99% rename from lib/cache/cache.js rename to lib/web/cache/cache.js index 74cd802de7f..acbd6c7d0f7 100644 --- a/lib/cache/cache.js +++ b/lib/web/cache/cache.js @@ -2,7 +2,7 @@ const { kConstruct } = require('./symbols') const { urlEquals, getFieldValues } = require('./util') -const { kEnumerableProperty, isDisturbed } = require('../core/util') +const { kEnumerableProperty, isDisturbed } = require('../../core/util') const { webidl } = require('../fetch/webidl') const { Response, cloneResponse, fromInnerResponse } = require('../fetch/response') const { Request, fromInnerRequest } = require('../fetch/request') @@ -10,7 +10,7 @@ const { kState } = require('../fetch/symbols') const { fetching } = require('../fetch/index') const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') const assert = require('node:assert') -const { getGlobalDispatcher } = require('../global') +const { getGlobalDispatcher } = require('../../global') /** * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation diff --git a/lib/cache/cachestorage.js b/lib/web/cache/cachestorage.js similarity index 98% rename from lib/cache/cachestorage.js rename to lib/web/cache/cachestorage.js index 4f3351a6a9b..de3813cfecb 100644 --- a/lib/cache/cachestorage.js +++ b/lib/web/cache/cachestorage.js @@ -3,7 +3,7 @@ const { kConstruct } = require('./symbols') const { Cache } = require('./cache') const { webidl } = require('../fetch/webidl') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') class CacheStorage { /** diff --git a/lib/web/cache/symbols.js b/lib/web/cache/symbols.js new file mode 100644 index 00000000000..9271fb61267 --- /dev/null +++ b/lib/web/cache/symbols.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + kConstruct: require('../../core/symbols').kConstruct +} diff --git a/lib/cache/util.js b/lib/web/cache/util.js similarity index 100% rename from lib/cache/util.js rename to lib/web/cache/util.js diff --git a/lib/cookies/constants.js b/lib/web/cookies/constants.js similarity index 100% rename from lib/cookies/constants.js rename to lib/web/cookies/constants.js diff --git a/lib/cookies/index.js b/lib/web/cookies/index.js similarity index 100% rename from lib/cookies/index.js rename to lib/web/cookies/index.js diff --git a/lib/cookies/parse.js b/lib/web/cookies/parse.js similarity index 100% rename from lib/cookies/parse.js rename to lib/web/cookies/parse.js diff --git a/lib/cookies/util.js b/lib/web/cookies/util.js similarity index 99% rename from lib/cookies/util.js rename to lib/web/cookies/util.js index 203c4bcf37c..0c1353d5ca3 100644 --- a/lib/cookies/util.js +++ b/lib/web/cookies/util.js @@ -1,7 +1,7 @@ 'use strict' const assert = require('node:assert') -const { kHeadersList } = require('../core/symbols') +const { kHeadersList } = require('../../core/symbols') function isCTLExcludingHtab (value) { if (value.length === 0) { diff --git a/lib/eventsource/eventsource-stream.js b/lib/web/eventsource/eventsource-stream.js similarity index 100% rename from lib/eventsource/eventsource-stream.js rename to lib/web/eventsource/eventsource-stream.js diff --git a/lib/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js similarity index 99% rename from lib/eventsource/eventsource.js rename to lib/web/eventsource/eventsource.js index ee61cfde880..ad6ea26dcd1 100644 --- a/lib/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -9,7 +9,7 @@ const { EventSourceStream } = require('./eventsource-stream') const { parseMIMEType } = require('../fetch/dataURL') const { MessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') -const { getGlobalDispatcher } = require('../global') +const { getGlobalDispatcher } = require('../../global') const { delay } = require('./util') let experimentalWarned = false diff --git a/lib/eventsource/util.js b/lib/web/eventsource/util.js similarity index 100% rename from lib/eventsource/util.js rename to lib/web/eventsource/util.js diff --git a/lib/fetch/LICENSE b/lib/web/fetch/LICENSE similarity index 100% rename from lib/fetch/LICENSE rename to lib/web/fetch/LICENSE diff --git a/lib/fetch/body.js b/lib/web/fetch/body.js similarity index 99% rename from lib/fetch/body.js rename to lib/web/fetch/body.js index 65fd63c5b23..4b81fcedc42 100644 --- a/lib/fetch/body.js +++ b/lib/web/fetch/body.js @@ -1,7 +1,7 @@ 'use strict' const Busboy = require('@fastify/busboy') -const util = require('../core/util') +const util = require('../../core/util') const { ReadableStreamFrom, isBlobLike, @@ -16,7 +16,7 @@ const { kState } = require('./symbols') const { webidl } = require('./webidl') const { Blob, File: NativeFile } = require('node:buffer') const assert = require('node:assert') -const { isErrored } = require('../core/util') +const { isErrored } = require('../../core/util') const { isArrayBuffer } = require('node:util/types') const { File: UndiciFile } = require('./file') const { serializeAMimeType } = require('./dataURL') diff --git a/lib/fetch/constants.js b/lib/web/fetch/constants.js similarity index 100% rename from lib/fetch/constants.js rename to lib/web/fetch/constants.js diff --git a/lib/fetch/dataURL.js b/lib/web/fetch/dataURL.js similarity index 100% rename from lib/fetch/dataURL.js rename to lib/web/fetch/dataURL.js diff --git a/lib/compat/dispatcher-weakref.js b/lib/web/fetch/dispatcher-weakref.js similarity index 93% rename from lib/compat/dispatcher-weakref.js rename to lib/web/fetch/dispatcher-weakref.js index 463b29ca319..05fde6f09f4 100644 --- a/lib/compat/dispatcher-weakref.js +++ b/lib/web/fetch/dispatcher-weakref.js @@ -1,6 +1,6 @@ 'use strict' -const { kConnected, kSize } = require('../core/symbols') +const { kConnected, kSize } = require('../../core/symbols') class CompatWeakRef { constructor (value) { diff --git a/lib/fetch/file.js b/lib/web/fetch/file.js similarity index 99% rename from lib/fetch/file.js rename to lib/web/fetch/file.js index d52feb3e3c9..61a232017b2 100644 --- a/lib/fetch/file.js +++ b/lib/web/fetch/file.js @@ -6,7 +6,7 @@ const { kState } = require('./symbols') const { isBlobLike } = require('./util') const { webidl } = require('./webidl') const { parseMIMEType, serializeAMimeType } = require('./dataURL') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') const encoder = new TextEncoder() class File extends Blob { diff --git a/lib/fetch/formdata.js b/lib/web/fetch/formdata.js similarity index 99% rename from lib/fetch/formdata.js rename to lib/web/fetch/formdata.js index 80df2b8f399..e8dcd6fa614 100644 --- a/lib/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -2,7 +2,7 @@ const { isBlobLike, iteratorMixin } = require('./util') const { kState } = require('./symbols') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') const { File: UndiciFile, FileLike, isFileLike } = require('./file') const { webidl } = require('./webidl') const { File: NativeFile } = require('node:buffer') diff --git a/lib/fetch/global.js b/lib/web/fetch/global.js similarity index 100% rename from lib/fetch/global.js rename to lib/web/fetch/global.js diff --git a/lib/fetch/headers.js b/lib/web/fetch/headers.js similarity index 99% rename from lib/fetch/headers.js rename to lib/web/fetch/headers.js index 8ee66fbf996..bdb53b52654 100644 --- a/lib/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -2,9 +2,9 @@ 'use strict' -const { kHeadersList, kConstruct } = require('../core/symbols') +const { kHeadersList, kConstruct } = require('../../core/symbols') const { kGuard } = require('./symbols') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') const { iteratorMixin, isValidHeaderName, diff --git a/lib/fetch/index.js b/lib/web/fetch/index.js similarity index 99% rename from lib/fetch/index.js rename to lib/web/fetch/index.js index f65bfbe78da..f4a6e5e6262 100644 --- a/lib/fetch/index.js +++ b/lib/web/fetch/index.js @@ -59,9 +59,9 @@ const { } = require('./constants') const EE = require('node:events') const { Readable, pipeline } = require('node:stream') -const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../core/util') +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../../core/util') const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./dataURL') -const { getGlobalDispatcher } = require('../global') +const { getGlobalDispatcher } = require('../../global') const { webidl } = require('./webidl') const { STATUS_CODES } = require('node:http') const GET_OR_HEAD = ['GET', 'HEAD'] diff --git a/lib/fetch/request.js b/lib/web/fetch/request.js similarity index 99% rename from lib/fetch/request.js rename to lib/web/fetch/request.js index 998e407e527..ef2f3ff0b14 100644 --- a/lib/fetch/request.js +++ b/lib/web/fetch/request.js @@ -4,8 +4,8 @@ const { extractBody, mixinBody, cloneBody } = require('./body') const { Headers, fill: fillHeaders, HeadersList } = require('./headers') -const { FinalizationRegistry } = require('../compat/dispatcher-weakref')() -const util = require('../core/util') +const { FinalizationRegistry } = require('./dispatcher-weakref')() +const util = require('../../core/util') const { isValidHTTPToken, sameOrigin, @@ -28,7 +28,7 @@ const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') const { webidl } = require('./webidl') const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') -const { kHeadersList, kConstruct } = require('../core/symbols') +const { kHeadersList, kConstruct } = require('../../core/symbols') const assert = require('node:assert') const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('node:events') diff --git a/lib/fetch/response.js b/lib/web/fetch/response.js similarity index 99% rename from lib/fetch/response.js rename to lib/web/fetch/response.js index 69aba18ba6e..355c2847aba 100644 --- a/lib/fetch/response.js +++ b/lib/web/fetch/response.js @@ -2,7 +2,7 @@ const { Headers, HeadersList, fill } = require('./headers') const { extractBody, cloneBody, mixinBody } = require('./body') -const util = require('../core/util') +const util = require('../../core/util') const { kEnumerableProperty } = util const { isValidReasonPhrase, @@ -22,7 +22,7 @@ const { webidl } = require('./webidl') const { FormData } = require('./formdata') const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') -const { kHeadersList, kConstruct } = require('../core/symbols') +const { kHeadersList, kConstruct } = require('../../core/symbols') const assert = require('node:assert') const { types } = require('node:util') diff --git a/lib/fetch/sort.js b/lib/web/fetch/sort.js similarity index 100% rename from lib/fetch/sort.js rename to lib/web/fetch/sort.js diff --git a/lib/fetch/symbols.js b/lib/web/fetch/symbols.js similarity index 100% rename from lib/fetch/symbols.js rename to lib/web/fetch/symbols.js diff --git a/lib/fetch/util.js b/lib/web/fetch/util.js similarity index 99% rename from lib/fetch/util.js rename to lib/web/fetch/util.js index c07db2338a4..b3523cb2e15 100644 --- a/lib/fetch/util.js +++ b/lib/web/fetch/util.js @@ -6,7 +6,7 @@ const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet const { getGlobalOrigin } = require('./global') const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./dataURL') const { performance } = require('node:perf_hooks') -const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../core/util') +const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../../core/util') const assert = require('node:assert') const { isUint8Array } = require('node:util/types') const { webidl } = require('./webidl') diff --git a/lib/fetch/webidl.js b/lib/web/fetch/webidl.js similarity index 99% rename from lib/fetch/webidl.js rename to lib/web/fetch/webidl.js index 0978e67f26f..da5df4a362f 100644 --- a/lib/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -1,7 +1,7 @@ 'use strict' const { types } = require('node:util') -const { toUSVString } = require('../core/util') +const { toUSVString } = require('../../core/util') /** @type {import('../../types/webidl').Webidl} */ const webidl = {} diff --git a/lib/fileapi/encoding.js b/lib/web/fileapi/encoding.js similarity index 100% rename from lib/fileapi/encoding.js rename to lib/web/fileapi/encoding.js diff --git a/lib/fileapi/filereader.js b/lib/web/fileapi/filereader.js similarity index 99% rename from lib/fileapi/filereader.js rename to lib/web/fileapi/filereader.js index cd36a22ff6f..0cca813994e 100644 --- a/lib/fileapi/filereader.js +++ b/lib/web/fileapi/filereader.js @@ -13,7 +13,7 @@ const { kAborted } = require('./symbols') const { webidl } = require('../fetch/webidl') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') class FileReader extends EventTarget { constructor () { diff --git a/lib/fileapi/progressevent.js b/lib/web/fileapi/progressevent.js similarity index 100% rename from lib/fileapi/progressevent.js rename to lib/web/fileapi/progressevent.js diff --git a/lib/fileapi/symbols.js b/lib/web/fileapi/symbols.js similarity index 100% rename from lib/fileapi/symbols.js rename to lib/web/fileapi/symbols.js diff --git a/lib/fileapi/util.js b/lib/web/fileapi/util.js similarity index 100% rename from lib/fileapi/util.js rename to lib/web/fileapi/util.js diff --git a/lib/websocket/connection.js b/lib/web/websocket/connection.js similarity index 98% rename from lib/websocket/connection.js rename to lib/web/websocket/connection.js index 399c3f52f14..33905404833 100644 --- a/lib/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -8,13 +8,13 @@ const { kReceivedClose } = require('./symbols') const { fireEvent, failWebsocketConnection } = require('./util') -const { channels } = require('../core/diagnostics') +const { channels } = require('../../core/diagnostics') const { CloseEvent } = require('./events') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') const { Headers } = require('../fetch/headers') -const { getGlobalDispatcher } = require('../global') -const { kHeadersList } = require('../core/symbols') +const { getGlobalDispatcher } = require('../../global') +const { kHeadersList } = require('../../core/symbols') /** @type {import('crypto')} */ let crypto diff --git a/lib/websocket/constants.js b/lib/web/websocket/constants.js similarity index 100% rename from lib/websocket/constants.js rename to lib/web/websocket/constants.js diff --git a/lib/websocket/events.js b/lib/web/websocket/events.js similarity index 99% rename from lib/websocket/events.js rename to lib/web/websocket/events.js index 626e8da0583..b1f91d0e190 100644 --- a/lib/websocket/events.js +++ b/lib/web/websocket/events.js @@ -1,7 +1,7 @@ 'use strict' const { webidl } = require('../fetch/webidl') -const { kEnumerableProperty } = require('../core/util') +const { kEnumerableProperty } = require('../../core/util') const { MessagePort } = require('node:worker_threads') /** diff --git a/lib/websocket/frame.js b/lib/web/websocket/frame.js similarity index 100% rename from lib/websocket/frame.js rename to lib/web/websocket/frame.js diff --git a/lib/websocket/receiver.js b/lib/web/websocket/receiver.js similarity index 99% rename from lib/websocket/receiver.js rename to lib/web/websocket/receiver.js index 28ff8b25f5c..63035618968 100644 --- a/lib/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -3,7 +3,7 @@ const { Writable } = require('node:stream') const { parserStates, opcodes, states, emptyBuffer } = require('./constants') const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols') -const { channels } = require('../core/diagnostics') +const { channels } = require('../../core/diagnostics') const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util') const { WebsocketFrameSend } = require('./frame') diff --git a/lib/websocket/symbols.js b/lib/web/websocket/symbols.js similarity index 100% rename from lib/websocket/symbols.js rename to lib/web/websocket/symbols.js diff --git a/lib/websocket/util.js b/lib/web/websocket/util.js similarity index 100% rename from lib/websocket/util.js rename to lib/web/websocket/util.js diff --git a/lib/websocket/websocket.js b/lib/web/websocket/websocket.js similarity index 99% rename from lib/websocket/websocket.js rename to lib/web/websocket/websocket.js index 6cbd01666ef..0072da48193 100644 --- a/lib/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -17,8 +17,8 @@ const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, f const { establishWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { ByteParser } = require('./receiver') -const { kEnumerableProperty, isBlobLike } = require('../core/util') -const { getGlobalDispatcher } = require('../global') +const { kEnumerableProperty, isBlobLike } = require('../../core/util') +const { getGlobalDispatcher } = require('../../global') const { types } = require('node:util') let experimentalWarned = false diff --git a/test/cache/get-field-values.js b/test/cache/get-field-values.js index 7a1a91d523a..b6a2f60dbbc 100644 --- a/test/cache/get-field-values.js +++ b/test/cache/get-field-values.js @@ -2,7 +2,7 @@ const { deepStrictEqual, throws } = require('node:assert') const { test } = require('node:test') -const { getFieldValues } = require('../../lib/cache/util') +const { getFieldValues } = require('../../lib/web/cache/util') test('getFieldValues', () => { throws(() => getFieldValues(null), { diff --git a/test/cookie/global-headers.js b/test/cookie/global-headers.js index 1dc9d9a5857..4afccc490ed 100644 --- a/test/cookie/global-headers.js +++ b/test/cookie/global-headers.js @@ -8,7 +8,7 @@ const { getSetCookies, setCookie } = require('../..') -const { getHeadersList } = require('../../lib/cookies/util') +const { getHeadersList } = require('../../lib/web/cookies/util') describe('Using global Headers', async () => { test('deleteCookies', () => { diff --git a/test/eventsource/eventsource-attributes.js b/test/eventsource/eventsource-attributes.js index 41b01aff295..0e046affdbd 100644 --- a/test/eventsource/eventsource-attributes.js +++ b/test/eventsource/eventsource-attributes.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - eventhandler idl', async () => { const server = http.createServer((req, res) => { diff --git a/test/eventsource/eventsource-close.js b/test/eventsource/eventsource-close.js index 7f88d00dc87..5b6397df63a 100644 --- a/test/eventsource/eventsource-close.js +++ b/test/eventsource/eventsource-close.js @@ -5,7 +5,7 @@ const events = require('node:events') const http = require('node:http') const { setTimeout } = require('node:timers/promises') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - close', () => { test('should not emit error when closing the EventSource Instance', async () => { diff --git a/test/eventsource/eventsource-connect.js b/test/eventsource/eventsource-connect.js index f5f81b1a549..75508ee954d 100644 --- a/test/eventsource/eventsource-connect.js +++ b/test/eventsource/eventsource-connect.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - sending correct request headers', () => { test('should send request with connection keep-alive', async () => { diff --git a/test/eventsource/eventsource-constructor-stringify.js b/test/eventsource/eventsource-constructor-stringify.js index 8e6fb7c2601..aee1d02bc6d 100644 --- a/test/eventsource/eventsource-constructor-stringify.js +++ b/test/eventsource/eventsource-constructor-stringify.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - constructor stringify', () => { test('should stringify argument', async () => { diff --git a/test/eventsource/eventsource-constructor.js b/test/eventsource/eventsource-constructor.js index 3640ba15467..a5e25e3f1dc 100644 --- a/test/eventsource/eventsource-constructor.js +++ b/test/eventsource/eventsource-constructor.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - withCredentials', () => { test('withCredentials should be false by default', async () => { diff --git a/test/eventsource/eventsource-message.js b/test/eventsource/eventsource-message.js index 8b76bdc6b26..d7843bc6891 100644 --- a/test/eventsource/eventsource-message.js +++ b/test/eventsource/eventsource-message.js @@ -5,7 +5,7 @@ const events = require('node:events') const http = require('node:http') const { setTimeout } = require('node:timers/promises') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - message', () => { test('Should not emit a message if only retry field was sent', async () => { diff --git a/test/eventsource/eventsource-reconnect.js b/test/eventsource/eventsource-reconnect.js index 3499b8b3702..50d5f6dadec 100644 --- a/test/eventsource/eventsource-reconnect.js +++ b/test/eventsource/eventsource-reconnect.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource, defaultReconnectionTime } = require('../../lib/eventsource/eventsource') +const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource') describe('EventSource - reconnect', () => { test('Should reconnect on connection close', async () => { diff --git a/test/eventsource/eventsource-redirecting.js b/test/eventsource/eventsource-redirecting.js index 1e8a31ae310..07bd36ea790 100644 --- a/test/eventsource/eventsource-redirecting.js +++ b/test/eventsource/eventsource-redirecting.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - redirecting', () => { [301, 302, 307, 308].forEach((statusCode) => { diff --git a/test/eventsource/eventsource-request-status-error.js b/test/eventsource/eventsource-request-status-error.js index a8775fde18a..3b73d22b851 100644 --- a/test/eventsource/eventsource-request-status-error.js +++ b/test/eventsource/eventsource-request-status-error.js @@ -4,7 +4,7 @@ const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - status error', () => { [204, 205, 210, 299, 404, 410, 503].forEach((statusCode) => { diff --git a/test/eventsource/eventsource-stream-bom.js b/test/eventsource/eventsource-stream-bom.js index 4bcfd76064f..b447832a124 100644 --- a/test/eventsource/eventsource-stream-bom.js +++ b/test/eventsource/eventsource-stream-bom.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test, describe } = require('node:test') -const { EventSourceStream } = require('../../lib/eventsource/eventsource-stream') +const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream - handle BOM', () => { test('Remove BOM from the beginning of the stream. 1 byte chunks', () => { diff --git a/test/eventsource/eventsource-stream-parse-line.js b/test/eventsource/eventsource-stream-parse-line.js index 6ef6dd8eca3..45069a2752d 100644 --- a/test/eventsource/eventsource-stream-parse-line.js +++ b/test/eventsource/eventsource-stream-parse-line.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test, describe } = require('node:test') -const { EventSourceStream } = require('../../lib/eventsource/eventsource-stream') +const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream - parseLine', () => { const defaultEventSourceSettings = { diff --git a/test/eventsource/eventsource-stream-process-event.js b/test/eventsource/eventsource-stream-process-event.js index aa106e15f1c..a0452ad36dc 100644 --- a/test/eventsource/eventsource-stream-process-event.js +++ b/test/eventsource/eventsource-stream-process-event.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test, describe } = require('node:test') -const { EventSourceStream } = require('../../lib/eventsource/eventsource-stream') +const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream - processEvent', () => { const defaultEventSourceSettings = { diff --git a/test/eventsource/eventsource-stream.js b/test/eventsource/eventsource-stream.js index 69a04821e26..8a74f539d8e 100644 --- a/test/eventsource/eventsource-stream.js +++ b/test/eventsource/eventsource-stream.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test, describe } = require('node:test') -const { EventSourceStream } = require('../../lib/eventsource/eventsource-stream') +const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream', () => { test('ignore empty chunks', () => { diff --git a/test/eventsource/eventsource.js b/test/eventsource/eventsource.js index 9ea9673f9e7..f7b62831449 100644 --- a/test/eventsource/eventsource.js +++ b/test/eventsource/eventsource.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/eventsource/eventsource') +const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - constructor', () => { test('Not providing url argument should throw', () => { diff --git a/test/eventsource/util.js b/test/eventsource/util.js index e976731557e..fa6c854a43a 100644 --- a/test/eventsource/util.js +++ b/test/eventsource/util.js @@ -2,7 +2,7 @@ const assert = require('node:assert') const { test } = require('node:test') -const { isASCIINumber, isValidLastEventId } = require('../../lib/eventsource/util') +const { isASCIINumber, isValidLastEventId } = require('../../lib/web/eventsource/util') test('isValidLastEventId', () => { assert.strictEqual(isValidLastEventId('valid'), true) diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js index de3c44a6eef..7c99c8e9422 100644 --- a/test/fetch/data-uri.js +++ b/test/fetch/data-uri.js @@ -9,7 +9,7 @@ const { stringPercentDecode, parseMIMEType, collectAnHTTPQuotedString -} = require('../../lib/fetch/dataURL') +} = require('../../lib/web/fetch/dataURL') const { fetch } = require('../..') test('https://url.spec.whatwg.org/#concept-url-serializer', async (t) => { diff --git a/test/fetch/file.js b/test/fetch/file.js index 44149653c12..4bf8e812d3b 100644 --- a/test/fetch/file.js +++ b/test/fetch/file.js @@ -4,7 +4,7 @@ const { Blob } = require('node:buffer') const { test } = require('node:test') const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') -const { File, FileLike } = require('../../lib/fetch/file') +const { File, FileLike } = require('../../lib/web/fetch/file') test('args validation', (t) => { const { throws, doesNotThrow, strictEqual } = tspl(t, { plan: 14 }) diff --git a/test/fetch/headers.js b/test/fetch/headers.js index d3e27fa348d..acdf5ab611e 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -3,8 +3,8 @@ const { test } = require('node:test') const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') -const { Headers, fill } = require('../../lib/fetch/headers') -const { kGuard } = require('../../lib/fetch/symbols') +const { Headers, fill } = require('../../lib/web/fetch/headers') +const { kGuard } = require('../../lib/web/fetch/symbols') const { once } = require('node:events') const { fetch } = require('../..') const { createServer } = require('node:http') diff --git a/test/fetch/headerslist-sortedarray.js b/test/fetch/headerslist-sortedarray.js index 72112f92288..9541901d2f2 100644 --- a/test/fetch/headerslist-sortedarray.js +++ b/test/fetch/headerslist-sortedarray.js @@ -2,7 +2,7 @@ const { test } = require('node:test') const assert = require('node:assert') -const { HeadersList, compareHeaderName } = require('../../lib/fetch/headers') +const { HeadersList, compareHeaderName } = require('../../lib/web/fetch/headers') const characters = 'abcdefghijklmnopqrstuvwxyz0123456789' const charactersLength = characters.length diff --git a/test/fetch/request.js b/test/fetch/request.js index f2855d07923..41ae8a836d7 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -10,12 +10,12 @@ const { Headers, fetch } = require('../../') -const { fromInnerRequest, makeRequest } = require('../../lib/fetch/request') +const { fromInnerRequest, makeRequest } = require('../../lib/web/fetch/request') const { Blob: ThirdPartyBlob, FormData: ThirdPartyFormData } = require('formdata-node') -const { kState, kGuard, kRealm, kSignal, kHeaders } = require('../../lib/fetch/symbols') +const { kState, kGuard, kRealm, kSignal, kHeaders } = require('../../lib/web/fetch/symbols') const { kHeadersList } = require('../../lib/core/symbols') const hasSignalReason = 'reason' in AbortSignal.prototype diff --git a/test/fetch/response.js b/test/fetch/response.js index cfd4ddb6268..1f44f99f15d 100644 --- a/test/fetch/response.js +++ b/test/fetch/response.js @@ -7,12 +7,12 @@ const { Response, FormData } = require('../../') -const { fromInnerResponse, makeResponse } = require('../../lib/fetch/response') +const { fromInnerResponse, makeResponse } = require('../../lib/web/fetch/response') const { Blob: ThirdPartyBlob, FormData: ThirdPartyFormData } = require('formdata-node') -const { kState, kGuard, kRealm, kHeaders } = require('../../lib/fetch/symbols') +const { kState, kGuard, kRealm, kHeaders } = require('../../lib/web/fetch/symbols') const { kHeadersList } = require('../../lib/core/symbols') test('arg validation', async () => { diff --git a/test/fetch/sort.js b/test/fetch/sort.js index a373c2a62ef..b2eef9ca699 100644 --- a/test/fetch/sort.js +++ b/test/fetch/sort.js @@ -2,7 +2,7 @@ const { describe, test } = require('node:test') const assert = require('node:assert') -const { sort, heapSort, binaryInsertionSort, introSort } = require('../../lib/fetch/sort') +const { sort, heapSort, binaryInsertionSort, introSort } = require('../../lib/web/fetch/sort') function generateRandomNumberArray (length) { const array = new Uint16Array(length) diff --git a/test/fetch/util.js b/test/fetch/util.js index eb245486f4d..a5c33d64934 100644 --- a/test/fetch/util.js +++ b/test/fetch/util.js @@ -3,8 +3,8 @@ const { test } = require('node:test') const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') -const util = require('../../lib/fetch/util') -const { HeadersList } = require('../../lib/fetch/headers') +const util = require('../../lib/web/fetch/util') +const { HeadersList } = require('../../lib/web/fetch/headers') const { createHash } = require('node:crypto') test('responseURL', (t) => { diff --git a/test/jest/instanceof-error.test.js b/test/jest/instanceof-error.test.js index 363ec7013e8..30667262dee 100644 --- a/test/jest/instanceof-error.test.js +++ b/test/jest/instanceof-error.test.js @@ -9,7 +9,7 @@ const { once } = require('node:events') jest.useRealTimers() it('isErrorLike sanity check', () => { - const { isErrorLike } = require('../../lib/fetch/util') + const { isErrorLike } = require('../../lib/web/fetch/util') const error = new DOMException('') // https://github.com/facebook/jest/issues/2549 diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index e4b13b1f482..3ba4a929af0 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -19,9 +19,9 @@ const { setGlobalDispatcher, Agent } = require('../../index.js') -const HeadersOrig = require('../../lib/fetch/headers.js').Headers -const ResponseOrig = require('../../lib/fetch/response.js').Response -const RequestOrig = require('../../lib/fetch/request.js').Request +const HeadersOrig = require('../../lib/web/fetch/headers.js').Headers +const ResponseOrig = require('../../lib/web/fetch/response.js').Response +const RequestOrig = require('../../lib/web/fetch/request.js').Request const TestServer = require('./utils/server.js') const { diff --git a/test/node-fetch/response.js b/test/node-fetch/response.js index e28dcb37119..84b3e9c471d 100644 --- a/test/node-fetch/response.js +++ b/test/node-fetch/response.js @@ -6,7 +6,7 @@ const stream = require('node:stream') const { Response } = require('../../index.js') const TestServer = require('./utils/server.js') const { Blob } = require('node:buffer') -const { kState } = require('../../lib/fetch/symbols.js') +const { kState } = require('../../lib/web/fetch/symbols.js') describe('Response', () => { const local = new TestServer() diff --git a/test/webidl/converters.js b/test/webidl/converters.js index cabd9185468..0e906ed6719 100644 --- a/test/webidl/converters.js +++ b/test/webidl/converters.js @@ -2,7 +2,7 @@ const { describe, test } = require('node:test') const assert = require('node:assert') -const { webidl } = require('../../lib/fetch/webidl') +const { webidl } = require('../../lib/web/fetch/webidl') test('sequence', () => { const converter = webidl.sequenceConverter( diff --git a/test/webidl/helpers.js b/test/webidl/helpers.js index 2842efeb3c2..b18baa5fb7b 100644 --- a/test/webidl/helpers.js +++ b/test/webidl/helpers.js @@ -2,7 +2,7 @@ const { describe, test } = require('node:test') const assert = require('node:assert') -const { webidl } = require('../../lib/fetch/webidl') +const { webidl } = require('../../lib/web/fetch/webidl') test('webidl.interfaceConverter', () => { class A {} diff --git a/test/webidl/util.js b/test/webidl/util.js index 1e087c53fee..83ea669fb2f 100644 --- a/test/webidl/util.js +++ b/test/webidl/util.js @@ -2,7 +2,7 @@ const { test } = require('node:test') const assert = require('node:assert') -const { webidl } = require('../../lib/fetch/webidl') +const { webidl } = require('../../lib/web/fetch/webidl') test('Type(V)', () => { const Type = webidl.util.Type diff --git a/test/websocket/events.js b/test/websocket/events.js index b7c533251aa..8186baac679 100644 --- a/test/websocket/events.js +++ b/test/websocket/events.js @@ -3,7 +3,7 @@ const { test, describe, after } = require('node:test') const assert = require('node:assert') const { WebSocketServer } = require('ws') -const { MessageEvent, CloseEvent, ErrorEvent } = require('../../lib/websocket/events') +const { MessageEvent, CloseEvent, ErrorEvent } = require('../../lib/web/websocket/events') const { WebSocket } = require('../..') test('MessageEvent', () => { diff --git a/test/websocket/frame.js b/test/websocket/frame.js index 936bd8f69e0..0f6a7d4a751 100644 --- a/test/websocket/frame.js +++ b/test/websocket/frame.js @@ -2,8 +2,8 @@ const { test } = require('node:test') const assert = require('node:assert') -const { WebsocketFrameSend } = require('../../lib/websocket/frame') -const { opcodes } = require('../../lib/websocket/constants') +const { WebsocketFrameSend } = require('../../lib/web/websocket/frame') +const { opcodes } = require('../../lib/web/websocket/constants') test('Writing 16-bit frame length value at correct offset when buffer has a non-zero byteOffset', () => { /* diff --git a/test/wpt/runner/worker.mjs b/test/wpt/runner/worker.mjs index 37c5dc6a105..f0f45542e97 100644 --- a/test/wpt/runner/worker.mjs +++ b/test/wpt/runner/worker.mjs @@ -7,13 +7,13 @@ import { parentPort, workerData } from 'node:worker_threads' import { fetch, File, FileReader, FormData, Headers, Request, Response, setGlobalOrigin } from '../../../index.js' -import { CloseEvent } from '../../../lib/websocket/events.js' -import { WebSocket } from '../../../lib/websocket/websocket.js' -import { Cache } from '../../../lib/cache/cache.js' -import { CacheStorage } from '../../../lib/cache/cachestorage.js' -import { kConstruct } from '../../../lib/cache/symbols.js' +import { CloseEvent } from '../../../lib/web/websocket/events.js' +import { WebSocket } from '../../../lib/web/websocket/websocket.js' +import { Cache } from '../../../lib/web/cache/cache.js' +import { CacheStorage } from '../../../lib/web/cache/cachestorage.js' +import { kConstruct } from '../../../lib/web/cache/symbols.js' // TODO(@KhafraDev): move this import once its added to index -import { EventSource } from '../../../lib/eventsource/eventsource.js' +import { EventSource } from '../../../lib/web/eventsource/eventsource.js' import { webcrypto } from 'node:crypto' const { initScripts, meta, test, url, path } = workerData From 34a138fa49abe744049d7803fedb038a19f7daf5 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Wed, 21 Feb 2024 22:37:58 -0800 Subject: [PATCH 056/123] `s/ dispactgher/dispatcher/` (#2807) --- docs/api/RetryAgent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/RetryAgent.md b/docs/api/RetryAgent.md index a1f38b3adb8..53ce5231315 100644 --- a/docs/api/RetryAgent.md +++ b/docs/api/RetryAgent.md @@ -9,7 +9,7 @@ Wraps a `undici.RetryHandler`. Arguments: -* **dispatcher** `undici.Dispatcher` (required) - the dispactgher to wrap +* **dispatcher** `undici.Dispatcher` (required) - the dispatcher to wrap * **options** `RetryHandlerOptions` (optional) - the options Returns: `ProxyAgent` From 5088696f3690c83c30c5e1ce6a128257021d02f5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 22 Feb 2024 08:12:07 +0100 Subject: [PATCH 057/123] Use paralellelRequests instead of connections to calculate req/sec in benchmarks (#2800) * Use paralellelRequests instead of connections to calculate req/sec in benchmarks Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * Revert "fixup" This reverts commit afb38789050a325199b77a24fc9a77ed70799dca. --------- Signed-off-by: Matteo Collina --- README.md | 38 +++++++++++++++++++++-------------- benchmarks/benchmark-http2.js | 37 ++++++---------------------------- benchmarks/benchmark-https.js | 2 +- benchmarks/benchmark.js | 2 +- benchmarks/server.js | 5 +++++ 5 files changed, 36 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e2ed1671cf4..821d7554f65 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,31 @@ npm i undici ## Benchmarks -The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a -50 TCP connections with a pipelining depth of 10 running on Node 20.10.0. +The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using: + +* 50 TCP connections +* A pipelining factor of 10 for undici +* 200 parallel requests issued per iteration (sample) + +The benchmark was run on Linux on top of Node 20.10.0. ``` -│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ -|─────────────────────|─────────|─────────────────|───────────|─────────────────────────| -│ got │ 45 │ 1661.71 req/sec │ ± 2.93 % │ - │ -│ node-fetch │ 20 │ 2164.81 req/sec │ ± 2.63 % │ + 30.28 % │ -│ undici - fetch │ 35 │ 2274.27 req/sec │ ± 2.70 % │ + 36.86 % │ -│ http - no keepalive │ 15 │ 2376.04 req/sec │ ± 2.99 % │ + 42.99 % │ -│ axios │ 25 │ 2612.93 req/sec │ ± 2.89 % │ + 57.24 % │ -│ request │ 40 │ 2712.19 req/sec │ ± 2.92 % │ + 63.22 % │ -│ http - keepalive │ 45 │ 4393.25 req/sec │ ± 2.86 % │ + 164.38 % │ -│ undici - pipeline │ 45 │ 5484.69 req/sec │ ± 2.87 % │ + 230.06 % │ -│ undici - request │ 55 │ 7773.98 req/sec │ ± 2.93 % │ + 367.83 % │ -│ undici - stream │ 70 │ 8425.96 req/sec │ ± 2.91 % │ + 407.07 % │ -│ undici - dispatch │ 50 │ 9488.99 req/sec │ ± 2.85 % │ + 471.04 % │ +┌─────────┬───────────────────────┬─────────┬────────────────────┬────────────┬─────────────────────────┐ +│ (index) │ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ +├─────────┼───────────────────────┼─────────┼────────────────────┼────────────┼─────────────────────────┤ +│ 0 │ 'got' │ 25 │ '3444.59 req/sec' │ '± 2.88 %' │ '-' │ +│ 1 │ 'node-fetch' │ 20 │ '4927.30 req/sec' │ '± 2.46 %' │ '+ 43.04 %' │ +│ 2 │ 'undici - fetch' │ 10 │ '5043.80 req/sec' │ '± 1.87 %' │ '+ 46.43 %' │ +│ 3 │ 'request' │ 35 │ '6389.13 req/sec' │ '± 2.93 %' │ '+ 85.48 %' │ +│ 4 │ 'axios' │ 25 │ '6920.61 req/sec' │ '± 2.76 %' │ '+ 100.91 %' │ +│ 5 │ 'http - no keepalive' │ 10 │ '9357.37 req/sec' │ '± 2.24 %' │ '+ 171.65 %' │ +│ 6 │ 'http - keepalive' │ 30 │ '9921.36 req/sec' │ '± 2.83 %' │ '+ 188.03 %' │ +│ 7 │ 'superagent' │ 10 │ '10118.35 req/sec' │ '± 2.18 %' │ '+ 193.75 %' │ +│ 8 │ 'undici - pipeline' │ 10 │ '17106.69 req/sec' │ '± 1.46 %' │ '+ 396.62 %' │ +│ 9 │ 'undici - request' │ 20 │ '21611.80 req/sec' │ '± 2.50 %' │ '+ 527.41 %' │ +│ 10 │ 'undici - stream' │ 10 │ '24282.13 req/sec' │ '± 1.94 %' │ '+ 604.94 %' │ +│ 11 │ 'undici - dispatch' │ 20 │ '24441.95 req/sec' │ '± 2.68 %' │ '+ 609.58 %' │ +└─────────┴───────────────────────┴─────────┴────────────────────┴────────────┴─────────────────────────┘ ``` ## Quick Start diff --git a/benchmarks/benchmark-http2.js b/benchmarks/benchmark-http2.js index 812fcea2e87..f89aacb993c 100644 --- a/benchmarks/benchmark-http2.js +++ b/benchmarks/benchmark-http2.js @@ -1,7 +1,5 @@ 'use strict' -const { connect } = require('node:http2') -const { createSecureContext } = require('node:tls') const os = require('node:os') const path = require('node:path') const { readFileSync } = require('node:fs') @@ -48,11 +46,6 @@ const httpsBaseOptions = { ...dest } -const http2ClientOptions = { - secureContext: createSecureContext({ ca }), - servername -} - const undiciOptions = { path: '/', method: 'GET', @@ -113,7 +106,9 @@ class SimpleRequest { } function makeParallelRequests (cb) { - return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) + const res = Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) + res.catch(console.error) + return res } function printResults (results) { @@ -143,10 +138,12 @@ function printResults (results) { last = mean } + console.log(mean) + return { Tests: name, Samples: size, - Result: `${((connections * 1e9) / mean).toFixed(2)} req/sec`, + Result: `${((1e9 * parallelRequests) / mean).toFixed(2)} req/sec`, Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`, 'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' } @@ -156,28 +153,6 @@ function printResults (results) { } const experiments = { - 'http2 - request' () { - return makeParallelRequests(resolve => { - connect(dest.url, http2ClientOptions, (session) => { - const headers = { - ':path': '/', - ':method': 'GET', - ':scheme': 'https', - ':authority': `localhost:${dest.port}` - } - - const request = session.request(headers) - - request.pipe( - new Writable({ - write (chunk, encoding, callback) { - callback() - } - }) - ).on('finish', resolve) - }) - }) - }, 'undici - pipeline' () { return makeParallelRequests(resolve => { dispatcher diff --git a/benchmarks/benchmark-https.js b/benchmarks/benchmark-https.js index 2f662bfbb54..ca0d5981bf0 100644 --- a/benchmarks/benchmark-https.js +++ b/benchmarks/benchmark-https.js @@ -160,7 +160,7 @@ function printResults (results) { return { Tests: name, Samples: size, - Result: `${((connections * 1e9) / mean).toFixed(2)} req/sec`, + Result: `${((parallelRequests * 1e9) / mean).toFixed(2)} req/sec`, Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`, 'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' } diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index f63ece5ed16..8775611138d 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -175,7 +175,7 @@ function printResults (results) { return { Tests: name, Samples: size, - Result: `${((connections * 1e9) / mean).toFixed(2)} req/sec`, + Result: `${((parallelRequests * 1e9) / mean).toFixed(2)} req/sec`, Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`, 'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' } diff --git a/benchmarks/server.js b/benchmarks/server.js index b6aaa8df52e..f35bdde97dc 100644 --- a/benchmarks/server.js +++ b/benchmarks/server.js @@ -24,10 +24,15 @@ if (cluster.isPrimary) { } } else { const buf = Buffer.alloc(64 * 1024, '_') + let i = 0 const server = createServer((req, res) => { + i++ setTimeout(function () { res.end(buf) }, timeout) }).listen(port) server.keepAliveTimeout = 600e3 + setInterval(() => { + console.log(`Worker ${process.pid} processed ${i} requests`) + }, 5000) } From 12a852e98fb1f4eed3023dcdd4889d3f24bb233e Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Thu, 22 Feb 2024 15:42:05 -0800 Subject: [PATCH 058/123] Split out documentation into separate directory (#2788) * split out documentation into seperate directory fix test/examples.js require fix requires temp symlink test update description * Update package.json Co-authored-by: Aras Abbasi --------- Co-authored-by: Aras Abbasi --- .github/dependabot.yml | 6 + .npmignore | 2 +- README.md | 437 +----------------- .nojekyll => documentation/.nojekyll | 0 CNAME => documentation/CNAME | 0 documentation/README.md | 428 +++++++++++++++++ {docs => documentation/docs}/api/Agent.md | 0 .../docs}/api/BalancedPool.md | 0 .../docs}/api/CacheStorage.md | 0 {docs => documentation/docs}/api/Client.md | 0 {docs => documentation/docs}/api/Connector.md | 0 .../docs}/api/ContentType.md | 0 {docs => documentation/docs}/api/Cookies.md | 0 {docs => documentation/docs}/api/Debug.md | 0 .../docs}/api/DiagnosticsChannel.md | 0 .../docs}/api/DispatchInterceptor.md | 0 .../docs}/api/Dispatcher.md | 0 {docs => documentation/docs}/api/Errors.md | 0 .../docs}/api/EventSource.md | 0 {docs => documentation/docs}/api/Fetch.md | 0 {docs => documentation/docs}/api/MockAgent.md | 0 .../docs}/api/MockClient.md | 0 .../docs}/api/MockErrors.md | 0 {docs => documentation/docs}/api/MockPool.md | 0 {docs => documentation/docs}/api/Pool.md | 0 {docs => documentation/docs}/api/PoolStats.md | 0 .../docs}/api/ProxyAgent.md | 0 .../docs}/api/RedirectHandler.md | 0 .../docs}/api/RetryAgent.md | 0 .../docs}/api/RetryHandler.md | 0 {docs => documentation/docs}/api/Util.md | 0 {docs => documentation/docs}/api/WebSocket.md | 0 .../docs}/api/api-lifecycle.md | 0 .../best-practices/client-certificate.md | 0 .../docs}/best-practices/mocking-request.md | 0 .../docs}/best-practices/proxy.md | 0 .../docs}/best-practices/writing-tests.md | 0 {docsify => documentation/docsify}/sidebar.md | 0 .../examples}/README.md | 0 .../examples}/ca-fingerprint/index.js | 2 +- .../examples}/eventsource.js | 2 +- {examples => documentation/examples}/fetch.js | 2 +- .../examples}/proxy-agent.js | 2 +- .../examples}/proxy/index.js | 2 +- .../examples}/proxy/proxy.js | 0 .../examples}/proxy/websocket.js | 2 +- .../examples}/request.js | 2 +- index.html => documentation/index.html | 0 documentation/package.json | 11 + package.json | 3 +- test/examples.js | 2 +- 51 files changed, 456 insertions(+), 447 deletions(-) mode change 100644 => 120000 README.md rename .nojekyll => documentation/.nojekyll (100%) rename CNAME => documentation/CNAME (100%) create mode 100644 documentation/README.md rename {docs => documentation/docs}/api/Agent.md (100%) rename {docs => documentation/docs}/api/BalancedPool.md (100%) rename {docs => documentation/docs}/api/CacheStorage.md (100%) rename {docs => documentation/docs}/api/Client.md (100%) rename {docs => documentation/docs}/api/Connector.md (100%) rename {docs => documentation/docs}/api/ContentType.md (100%) rename {docs => documentation/docs}/api/Cookies.md (100%) rename {docs => documentation/docs}/api/Debug.md (100%) rename {docs => documentation/docs}/api/DiagnosticsChannel.md (100%) rename {docs => documentation/docs}/api/DispatchInterceptor.md (100%) rename {docs => documentation/docs}/api/Dispatcher.md (100%) rename {docs => documentation/docs}/api/Errors.md (100%) rename {docs => documentation/docs}/api/EventSource.md (100%) rename {docs => documentation/docs}/api/Fetch.md (100%) rename {docs => documentation/docs}/api/MockAgent.md (100%) rename {docs => documentation/docs}/api/MockClient.md (100%) rename {docs => documentation/docs}/api/MockErrors.md (100%) rename {docs => documentation/docs}/api/MockPool.md (100%) rename {docs => documentation/docs}/api/Pool.md (100%) rename {docs => documentation/docs}/api/PoolStats.md (100%) rename {docs => documentation/docs}/api/ProxyAgent.md (100%) rename {docs => documentation/docs}/api/RedirectHandler.md (100%) rename {docs => documentation/docs}/api/RetryAgent.md (100%) rename {docs => documentation/docs}/api/RetryHandler.md (100%) rename {docs => documentation/docs}/api/Util.md (100%) rename {docs => documentation/docs}/api/WebSocket.md (100%) rename {docs => documentation/docs}/api/api-lifecycle.md (100%) rename {docs => documentation/docs}/best-practices/client-certificate.md (100%) rename {docs => documentation/docs}/best-practices/mocking-request.md (100%) rename {docs => documentation/docs}/best-practices/proxy.md (100%) rename {docs => documentation/docs}/best-practices/writing-tests.md (100%) rename {docsify => documentation/docsify}/sidebar.md (100%) rename {examples => documentation/examples}/README.md (100%) rename {examples => documentation/examples}/ca-fingerprint/index.js (97%) rename {examples => documentation/examples}/eventsource.js (92%) rename {examples => documentation/examples}/fetch.js (87%) rename {examples => documentation/examples}/proxy-agent.js (98%) rename {examples => documentation/examples}/proxy/index.js (95%) rename {examples => documentation/examples}/proxy/proxy.js (100%) rename {examples => documentation/examples}/proxy/websocket.js (97%) rename {examples => documentation/examples}/request.js (98%) rename index.html => documentation/index.html (100%) create mode 100644 documentation/package.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 18b9fbf0503..ec90956a8ea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,12 @@ updates: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "npm" + directory: /documentation + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + - package-ecosystem: docker directory: /build schedule: diff --git a/.npmignore b/.npmignore index c55b19ffd15..a00ceea145e 100644 --- a/.npmignore +++ b/.npmignore @@ -9,4 +9,4 @@ lib/llhttp/llhttp.wasm !types/**/* !index.d.ts -!docs/**/* +!documentation/docs/**/* diff --git a/README.md b/README.md deleted file mode 100644 index 821d7554f65..00000000000 --- a/README.md +++ /dev/null @@ -1,436 +0,0 @@ -# undici - -[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici) - -An HTTP/1.1 client, written from scratch for Node.js. - -> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici. -It is also a Stranger Things reference. - -Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel. - -## Install - -``` -npm i undici -``` - -## Benchmarks - -The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using: - -* 50 TCP connections -* A pipelining factor of 10 for undici -* 200 parallel requests issued per iteration (sample) - -The benchmark was run on Linux on top of Node 20.10.0. - -``` -┌─────────┬───────────────────────┬─────────┬────────────────────┬────────────┬─────────────────────────┐ -│ (index) │ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ -├─────────┼───────────────────────┼─────────┼────────────────────┼────────────┼─────────────────────────┤ -│ 0 │ 'got' │ 25 │ '3444.59 req/sec' │ '± 2.88 %' │ '-' │ -│ 1 │ 'node-fetch' │ 20 │ '4927.30 req/sec' │ '± 2.46 %' │ '+ 43.04 %' │ -│ 2 │ 'undici - fetch' │ 10 │ '5043.80 req/sec' │ '± 1.87 %' │ '+ 46.43 %' │ -│ 3 │ 'request' │ 35 │ '6389.13 req/sec' │ '± 2.93 %' │ '+ 85.48 %' │ -│ 4 │ 'axios' │ 25 │ '6920.61 req/sec' │ '± 2.76 %' │ '+ 100.91 %' │ -│ 5 │ 'http - no keepalive' │ 10 │ '9357.37 req/sec' │ '± 2.24 %' │ '+ 171.65 %' │ -│ 6 │ 'http - keepalive' │ 30 │ '9921.36 req/sec' │ '± 2.83 %' │ '+ 188.03 %' │ -│ 7 │ 'superagent' │ 10 │ '10118.35 req/sec' │ '± 2.18 %' │ '+ 193.75 %' │ -│ 8 │ 'undici - pipeline' │ 10 │ '17106.69 req/sec' │ '± 1.46 %' │ '+ 396.62 %' │ -│ 9 │ 'undici - request' │ 20 │ '21611.80 req/sec' │ '± 2.50 %' │ '+ 527.41 %' │ -│ 10 │ 'undici - stream' │ 10 │ '24282.13 req/sec' │ '± 1.94 %' │ '+ 604.94 %' │ -│ 11 │ 'undici - dispatch' │ 20 │ '24441.95 req/sec' │ '± 2.68 %' │ '+ 609.58 %' │ -└─────────┴───────────────────────┴─────────┴────────────────────┴────────────┴─────────────────────────┘ -``` - -## Quick Start - -```js -import { request } from 'undici' - -const { - statusCode, - headers, - trailers, - body -} = await request('http://localhost:3000/foo') - -console.log('response received', statusCode) -console.log('headers', headers) - -for await (const data of body) { console.log('data', data) } - -console.log('trailers', trailers) -``` - -## Body Mixins - -The `body` mixins are the most common way to format the request/response body. Mixins include: - -- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) -- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) -- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) - -Example usage: - -```js -import { request } from 'undici' - -const { - statusCode, - headers, - trailers, - body -} = await request('http://localhost:3000/foo') - -console.log('response received', statusCode) -console.log('headers', headers) -console.log('data', await body.json()) -console.log('trailers', trailers) -``` - -_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._ - -Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format. - -For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). - -## Common API Methods - -This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site. - -### `undici.request([url, options]): Promise` - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` - * **maxRedirections** `Integer` - Default: `0` - -Returns a promise with the result of the `Dispatcher.request` method. - -Calls `options.dispatcher.request(options)`. - -See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples. - -### `undici.stream([url, options, ]factory): Promise` - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` - * **maxRedirections** `Integer` - Default: `0` -* **factory** `Dispatcher.stream.factory` - -Returns a promise with the result of the `Dispatcher.stream` method. - -Calls `options.dispatcher.stream(options, factory)`. - -See [Dispatcher.stream](docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details. - -### `undici.pipeline([url, options, ]handler): Duplex` - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`PipelineOptions`](docs/api/Dispatcher.md#parameter-pipelineoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` - * **maxRedirections** `Integer` - Default: `0` -* **handler** `Dispatcher.pipeline.handler` - -Returns: `stream.Duplex` - -Calls `options.dispatch.pipeline(options, handler)`. - -See [Dispatcher.pipeline](docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details. - -### `undici.connect([url, options]): Promise` - -Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT). - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`ConnectOptions`](docs/api/Dispatcher.md#parameter-connectoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **maxRedirections** `Integer` - Default: `0` -* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional) - -Returns a promise with the result of the `Dispatcher.connect` method. - -Calls `options.dispatch.connect(options)`. - -See [Dispatcher.connect](docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details. - -### `undici.fetch(input[, init]): Promise` - -Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method). - -* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -* https://fetch.spec.whatwg.org/#fetch-method - -Basic usage example: - -```js -import { fetch } from 'undici' - - -const res = await fetch('https://example.com') -const json = await res.json() -console.log(json) -``` - -You can pass an optional dispatcher to `fetch` as: - -```js -import { fetch, Agent } from 'undici' - -const res = await fetch('https://example.com', { - // Mocks are also supported - dispatcher: new Agent({ - keepAliveTimeout: 10, - keepAliveMaxTimeout: 10 - }) -}) -const json = await res.json() -console.log(json) -``` - -#### `request.body` - -A body can be of the following types: - -- ArrayBuffer -- ArrayBufferView -- AsyncIterables -- Blob -- Iterables -- String -- URLSearchParams -- FormData - -In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org) - -```js -import { fetch } from 'undici' - -const data = { - async *[Symbol.asyncIterator]() { - yield 'hello' - yield 'world' - }, -} - -await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' }) -``` - -#### `request.duplex` - -- half - -In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex) - -#### `response.body` - -Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`. - -```js -import { fetch } from 'undici' -import { Readable } from 'node:stream' - -const response = await fetch('https://example.com') -const readableWebStream = response.body -const readableNodeStream = Readable.fromWeb(readableWebStream) -``` - -#### Specification Compliance - -This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does -not support or does not fully implement. - -##### Garbage Collection - -* https://fetch.spec.whatwg.org/#garbage-collection - -The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on -[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body. - -Garbage collection in Node is less aggressive and deterministic -(due to the lack of clear idle periods that browsers have through the rendering refresh rate) -which means that leaving the release of connection resources to the garbage collector can lead -to excessive connection usage, reduced performance (due to less connection re-use), and even -stalls or deadlocks when running out of connections. - -```js -// Do -const headers = await fetch(url) - .then(async res => { - for await (const chunk of res.body) { - // force consumption of body - } - return res.headers - }) - -// Do not -const headers = await fetch(url) - .then(res => res.headers) -``` - -However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details. - -```js -const headers = await fetch(url, { method: 'HEAD' }) - .then(res => res.headers) -``` - -##### Forbidden and Safelisted Header Names - -* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name -* https://fetch.spec.whatwg.org/#forbidden-header-name -* https://fetch.spec.whatwg.org/#forbidden-response-header-name -* https://github.com/wintercg/fetch/issues/6 - -The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user. - -### `undici.upgrade([url, options]): Promise` - -Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details. - -Arguments: - -* **url** `string | URL | UrlObject` -* **options** [`UpgradeOptions`](docs/api/Dispatcher.md#parameter-upgradeoptions) - * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) - * **maxRedirections** `Integer` - Default: `0` -* **callback** `(error: Error | null, data: UpgradeData) => void` (optional) - -Returns a promise with the result of the `Dispatcher.upgrade` method. - -Calls `options.dispatcher.upgrade(options)`. - -See [Dispatcher.upgrade](docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details. - -### `undici.setGlobalDispatcher(dispatcher)` - -* dispatcher `Dispatcher` - -Sets the global dispatcher used by Common API Methods. - -### `undici.getGlobalDispatcher()` - -Gets the global dispatcher used by Common API Methods. - -Returns: `Dispatcher` - -### `undici.setGlobalOrigin(origin)` - -* origin `string | URL | undefined` - -Sets the global origin used in `fetch`. - -If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed. - -```js -setGlobalOrigin('http://localhost:3000') - -const response = await fetch('/api/ping') - -console.log(response.url) // http://localhost:3000/api/ping -``` - -### `undici.getGlobalOrigin()` - -Gets the global origin used in `fetch`. - -Returns: `URL` - -### `UrlObject` - -* **port** `string | number` (optional) -* **path** `string` (optional) -* **pathname** `string` (optional) -* **hostname** `string` (optional) -* **origin** `string` (optional) -* **protocol** `string` (optional) -* **search** `string` (optional) - -## Specification Compliance - -This section documents parts of the HTTP/1.1 specification that Undici does -not support or does not fully implement. - -### Expect - -Undici does not support the `Expect` request header field. The request -body is always immediately sent and the `100 Continue` response will be -ignored. - -Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1 - -### Pipelining - -Undici will only use pipelining if configured with a `pipelining` factor -greater than `1`. - -Undici always assumes that connections are persistent and will immediately -pipeline requests, without checking whether the connection is persistent. -Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is -not supported. - -Undici will immediately pipeline when retrying requests after a failed -connection. However, Undici will not retry the first remaining requests in -the prior pipeline and instead error the corresponding callback/promise/stream. - -Undici will abort all running requests in the pipeline when any of them are -aborted. - -* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2 -* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2 - -### Manual Redirect - -Since it is not possible to manually follow an HTTP redirect on the server-side, -Undici returns the actual response instead of an `opaqueredirect` filtered one -when invoked with a `manual` redirect. This aligns `fetch()` with the other -implementations in Deno and Cloudflare Workers. - -Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling - -## Workarounds - -### Network address family autoselection. - -If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record) -first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case -undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`. - -If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version -(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request` -and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection. - -## Collaborators - -* [__Daniele Belardi__](https://github.com/dnlup), -* [__Ethan Arrowood__](https://github.com/ethan-arrowood), -* [__Matteo Collina__](https://github.com/mcollina), -* [__Matthew Aitken__](https://github.com/KhafraDev), -* [__Robert Nagy__](https://github.com/ronag), -* [__Szymon Marczak__](https://github.com/szmarczak), -* [__Tomas Della Vedova__](https://github.com/delvedor), - -### Releasers - -* [__Ethan Arrowood__](https://github.com/ethan-arrowood), -* [__Matteo Collina__](https://github.com/mcollina), -* [__Robert Nagy__](https://github.com/ronag), -* [__Matthew Aitken__](https://github.com/KhafraDev), - -## License - -MIT diff --git a/README.md b/README.md new file mode 120000 index 00000000000..c95b8a6a153 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +documentation/README.md \ No newline at end of file diff --git a/.nojekyll b/documentation/.nojekyll similarity index 100% rename from .nojekyll rename to documentation/.nojekyll diff --git a/CNAME b/documentation/CNAME similarity index 100% rename from CNAME rename to documentation/CNAME diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 00000000000..144b4cb6534 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,428 @@ +# undici + +[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici) + +An HTTP/1.1 client, written from scratch for Node.js. + +> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici. +It is also a Stranger Things reference. + +Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel. + +## Install + +``` +npm i undici +``` + +## Benchmarks + +The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a +50 TCP connections with a pipelining depth of 10 running on Node 20.10.0. + +``` +│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ +|─────────────────────|─────────|─────────────────|───────────|─────────────────────────| +│ got │ 45 │ 1661.71 req/sec │ ± 2.93 % │ - │ +│ node-fetch │ 20 │ 2164.81 req/sec │ ± 2.63 % │ + 30.28 % │ +│ undici - fetch │ 35 │ 2274.27 req/sec │ ± 2.70 % │ + 36.86 % │ +│ http - no keepalive │ 15 │ 2376.04 req/sec │ ± 2.99 % │ + 42.99 % │ +│ axios │ 25 │ 2612.93 req/sec │ ± 2.89 % │ + 57.24 % │ +│ request │ 40 │ 2712.19 req/sec │ ± 2.92 % │ + 63.22 % │ +│ http - keepalive │ 45 │ 4393.25 req/sec │ ± 2.86 % │ + 164.38 % │ +│ undici - pipeline │ 45 │ 5484.69 req/sec │ ± 2.87 % │ + 230.06 % │ +│ undici - request │ 55 │ 7773.98 req/sec │ ± 2.93 % │ + 367.83 % │ +│ undici - stream │ 70 │ 8425.96 req/sec │ ± 2.91 % │ + 407.07 % │ +│ undici - dispatch │ 50 │ 9488.99 req/sec │ ± 2.85 % │ + 471.04 % │ +``` + +## Quick Start + +```js +import { request } from 'undici' + +const { + statusCode, + headers, + trailers, + body +} = await request('http://localhost:3000/foo') + +console.log('response received', statusCode) +console.log('headers', headers) + +for await (const data of body) { console.log('data', data) } + +console.log('trailers', trailers) +``` + +## Body Mixins + +The `body` mixins are the most common way to format the request/response body. Mixins include: + +- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) +- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) +- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) + +Example usage: + +```js +import { request } from 'undici' + +const { + statusCode, + headers, + trailers, + body +} = await request('http://localhost:3000/foo') + +console.log('response received', statusCode) +console.log('headers', headers) +console.log('data', await body.json()) +console.log('trailers', trailers) +``` + +_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._ + +Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format. + +For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). + +## Common API Methods + +This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [documentation](./documentation/) folder and are accessible via the navigation list on the left side of the docs site. + +### `undici.request([url, options]): Promise` + +Arguments: + +* **url** `string | URL | UrlObject` +* **options** [`RequestOptions`](./documentation/docs/api/Dispatcher.md#parameter-requestoptions) + * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) + * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` + * **maxRedirections** `Integer` - Default: `0` + +Returns a promise with the result of the `Dispatcher.request` method. + +Calls `options.dispatcher.request(options)`. + +See [Dispatcher.request](./documentation/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./documentation/examples/README.md) for examples. + +### `undici.stream([url, options, ]factory): Promise` + +Arguments: + +* **url** `string | URL | UrlObject` +* **options** [`StreamOptions`](./documentation/docs/api/Dispatcher.md#parameter-streamoptions) + * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) + * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` + * **maxRedirections** `Integer` - Default: `0` +* **factory** `Dispatcher.stream.factory` + +Returns a promise with the result of the `Dispatcher.stream` method. + +Calls `options.dispatcher.stream(options, factory)`. + +See [Dispatcher.stream](documentation/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details. + +### `undici.pipeline([url, options, ]handler): Duplex` + +Arguments: + +* **url** `string | URL | UrlObject` +* **options** [`PipelineOptions`](documentation/docs/api/Dispatcher.md#parameter-pipelineoptions) + * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) + * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET` + * **maxRedirections** `Integer` - Default: `0` +* **handler** `Dispatcher.pipeline.handler` + +Returns: `stream.Duplex` + +Calls `options.dispatch.pipeline(options, handler)`. + +See [Dispatcher.pipeline](documentation/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details. + +### `undici.connect([url, options]): Promise` + +Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT). + +Arguments: + +* **url** `string | URL | UrlObject` +* **options** [`ConnectOptions`](documentation/docs/api/Dispatcher.md#parameter-connectoptions) + * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) + * **maxRedirections** `Integer` - Default: `0` +* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional) + +Returns a promise with the result of the `Dispatcher.connect` method. + +Calls `options.dispatch.connect(options)`. + +See [Dispatcher.connect](documentation/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details. + +### `undici.fetch(input[, init]): Promise` + +Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method). + +* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch +* https://fetch.spec.whatwg.org/#fetch-method + +Basic usage example: + +```js +import { fetch } from 'undici' + + +const res = await fetch('https://example.com') +const json = await res.json() +console.log(json) +``` + +You can pass an optional dispatcher to `fetch` as: + +```js +import { fetch, Agent } from 'undici' + +const res = await fetch('https://example.com', { + // Mocks are also supported + dispatcher: new Agent({ + keepAliveTimeout: 10, + keepAliveMaxTimeout: 10 + }) +}) +const json = await res.json() +console.log(json) +``` + +#### `request.body` + +A body can be of the following types: + +- ArrayBuffer +- ArrayBufferView +- AsyncIterables +- Blob +- Iterables +- String +- URLSearchParams +- FormData + +In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org) + +```js +import { fetch } from 'undici' + +const data = { + async *[Symbol.asyncIterator]() { + yield 'hello' + yield 'world' + }, +} + +await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' }) +``` + +#### `request.duplex` + +- half + +In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex) + +#### `response.body` + +Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`. + +```js +import { fetch } from 'undici' +import { Readable } from 'node:stream' + +const response = await fetch('https://example.com') +const readableWebStream = response.body +const readableNodeStream = Readable.fromWeb(readableWebStream) +``` + +#### Specification Compliance + +This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does +not support or does not fully implement. + +##### Garbage Collection + +* https://fetch.spec.whatwg.org/#garbage-collection + +The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on +[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body. + +Garbage collection in Node is less aggressive and deterministic +(due to the lack of clear idle periods that browsers have through the rendering refresh rate) +which means that leaving the release of connection resources to the garbage collector can lead +to excessive connection usage, reduced performance (due to less connection re-use), and even +stalls or deadlocks when running out of connections. + +```js +// Do +const headers = await fetch(url) + .then(async res => { + for await (const chunk of res.body) { + // force consumption of body + } + return res.headers + }) + +// Do not +const headers = await fetch(url) + .then(res => res.headers) +``` + +However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details. + +```js +const headers = await fetch(url, { method: 'HEAD' }) + .then(res => res.headers) +``` + +##### Forbidden and Safelisted Header Names + +* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name +* https://fetch.spec.whatwg.org/#forbidden-header-name +* https://fetch.spec.whatwg.org/#forbidden-response-header-name +* https://github.com/wintercg/fetch/issues/6 + +The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user. + +### `undici.upgrade([url, options]): Promise` + +Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details. + +Arguments: + +* **url** `string | URL | UrlObject` +* **options** [`UpgradeOptions`](documentation/docs/api/Dispatcher.md#parameter-upgradeoptions) + * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher) + * **maxRedirections** `Integer` - Default: `0` +* **callback** `(error: Error | null, data: UpgradeData) => void` (optional) + +Returns a promise with the result of the `Dispatcher.upgrade` method. + +Calls `options.dispatcher.upgrade(options)`. + +See [Dispatcher.upgrade](documentation/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details. + +### `undici.setGlobalDispatcher(dispatcher)` + +* dispatcher `Dispatcher` + +Sets the global dispatcher used by Common API Methods. + +### `undici.getGlobalDispatcher()` + +Gets the global dispatcher used by Common API Methods. + +Returns: `Dispatcher` + +### `undici.setGlobalOrigin(origin)` + +* origin `string | URL | undefined` + +Sets the global origin used in `fetch`. + +If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed. + +```js +setGlobalOrigin('http://localhost:3000') + +const response = await fetch('/api/ping') + +console.log(response.url) // http://localhost:3000/api/ping +``` + +### `undici.getGlobalOrigin()` + +Gets the global origin used in `fetch`. + +Returns: `URL` + +### `UrlObject` + +* **port** `string | number` (optional) +* **path** `string` (optional) +* **pathname** `string` (optional) +* **hostname** `string` (optional) +* **origin** `string` (optional) +* **protocol** `string` (optional) +* **search** `string` (optional) + +## Specification Compliance + +This section documents parts of the HTTP/1.1 specification that Undici does +not support or does not fully implement. + +### Expect + +Undici does not support the `Expect` request header field. The request +body is always immediately sent and the `100 Continue` response will be +ignored. + +Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1 + +### Pipelining + +Undici will only use pipelining if configured with a `pipelining` factor +greater than `1`. + +Undici always assumes that connections are persistent and will immediately +pipeline requests, without checking whether the connection is persistent. +Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is +not supported. + +Undici will immediately pipeline when retrying requests after a failed +connection. However, Undici will not retry the first remaining requests in +the prior pipeline and instead error the corresponding callback/promise/stream. + +Undici will abort all running requests in the pipeline when any of them are +aborted. + +* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2 +* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2 + +### Manual Redirect + +Since it is not possible to manually follow an HTTP redirect on the server-side, +Undici returns the actual response instead of an `opaqueredirect` filtered one +when invoked with a `manual` redirect. This aligns `fetch()` with the other +implementations in Deno and Cloudflare Workers. + +Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling + +## Workarounds + +### Network address family autoselection. + +If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record) +first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case +undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`. + +If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version +(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request` +and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection. + +## Collaborators + +* [__Daniele Belardi__](https://github.com/dnlup), +* [__Ethan Arrowood__](https://github.com/ethan-arrowood), +* [__Matteo Collina__](https://github.com/mcollina), +* [__Matthew Aitken__](https://github.com/KhafraDev), +* [__Robert Nagy__](https://github.com/ronag), +* [__Szymon Marczak__](https://github.com/szmarczak), +* [__Tomas Della Vedova__](https://github.com/delvedor), + +### Releasers + +* [__Ethan Arrowood__](https://github.com/ethan-arrowood), +* [__Matteo Collina__](https://github.com/mcollina), +* [__Robert Nagy__](https://github.com/ronag), +* [__Matthew Aitken__](https://github.com/KhafraDev), + +## License + +MIT diff --git a/docs/api/Agent.md b/documentation/docs/api/Agent.md similarity index 100% rename from docs/api/Agent.md rename to documentation/docs/api/Agent.md diff --git a/docs/api/BalancedPool.md b/documentation/docs/api/BalancedPool.md similarity index 100% rename from docs/api/BalancedPool.md rename to documentation/docs/api/BalancedPool.md diff --git a/docs/api/CacheStorage.md b/documentation/docs/api/CacheStorage.md similarity index 100% rename from docs/api/CacheStorage.md rename to documentation/docs/api/CacheStorage.md diff --git a/docs/api/Client.md b/documentation/docs/api/Client.md similarity index 100% rename from docs/api/Client.md rename to documentation/docs/api/Client.md diff --git a/docs/api/Connector.md b/documentation/docs/api/Connector.md similarity index 100% rename from docs/api/Connector.md rename to documentation/docs/api/Connector.md diff --git a/docs/api/ContentType.md b/documentation/docs/api/ContentType.md similarity index 100% rename from docs/api/ContentType.md rename to documentation/docs/api/ContentType.md diff --git a/docs/api/Cookies.md b/documentation/docs/api/Cookies.md similarity index 100% rename from docs/api/Cookies.md rename to documentation/docs/api/Cookies.md diff --git a/docs/api/Debug.md b/documentation/docs/api/Debug.md similarity index 100% rename from docs/api/Debug.md rename to documentation/docs/api/Debug.md diff --git a/docs/api/DiagnosticsChannel.md b/documentation/docs/api/DiagnosticsChannel.md similarity index 100% rename from docs/api/DiagnosticsChannel.md rename to documentation/docs/api/DiagnosticsChannel.md diff --git a/docs/api/DispatchInterceptor.md b/documentation/docs/api/DispatchInterceptor.md similarity index 100% rename from docs/api/DispatchInterceptor.md rename to documentation/docs/api/DispatchInterceptor.md diff --git a/docs/api/Dispatcher.md b/documentation/docs/api/Dispatcher.md similarity index 100% rename from docs/api/Dispatcher.md rename to documentation/docs/api/Dispatcher.md diff --git a/docs/api/Errors.md b/documentation/docs/api/Errors.md similarity index 100% rename from docs/api/Errors.md rename to documentation/docs/api/Errors.md diff --git a/docs/api/EventSource.md b/documentation/docs/api/EventSource.md similarity index 100% rename from docs/api/EventSource.md rename to documentation/docs/api/EventSource.md diff --git a/docs/api/Fetch.md b/documentation/docs/api/Fetch.md similarity index 100% rename from docs/api/Fetch.md rename to documentation/docs/api/Fetch.md diff --git a/docs/api/MockAgent.md b/documentation/docs/api/MockAgent.md similarity index 100% rename from docs/api/MockAgent.md rename to documentation/docs/api/MockAgent.md diff --git a/docs/api/MockClient.md b/documentation/docs/api/MockClient.md similarity index 100% rename from docs/api/MockClient.md rename to documentation/docs/api/MockClient.md diff --git a/docs/api/MockErrors.md b/documentation/docs/api/MockErrors.md similarity index 100% rename from docs/api/MockErrors.md rename to documentation/docs/api/MockErrors.md diff --git a/docs/api/MockPool.md b/documentation/docs/api/MockPool.md similarity index 100% rename from docs/api/MockPool.md rename to documentation/docs/api/MockPool.md diff --git a/docs/api/Pool.md b/documentation/docs/api/Pool.md similarity index 100% rename from docs/api/Pool.md rename to documentation/docs/api/Pool.md diff --git a/docs/api/PoolStats.md b/documentation/docs/api/PoolStats.md similarity index 100% rename from docs/api/PoolStats.md rename to documentation/docs/api/PoolStats.md diff --git a/docs/api/ProxyAgent.md b/documentation/docs/api/ProxyAgent.md similarity index 100% rename from docs/api/ProxyAgent.md rename to documentation/docs/api/ProxyAgent.md diff --git a/docs/api/RedirectHandler.md b/documentation/docs/api/RedirectHandler.md similarity index 100% rename from docs/api/RedirectHandler.md rename to documentation/docs/api/RedirectHandler.md diff --git a/docs/api/RetryAgent.md b/documentation/docs/api/RetryAgent.md similarity index 100% rename from docs/api/RetryAgent.md rename to documentation/docs/api/RetryAgent.md diff --git a/docs/api/RetryHandler.md b/documentation/docs/api/RetryHandler.md similarity index 100% rename from docs/api/RetryHandler.md rename to documentation/docs/api/RetryHandler.md diff --git a/docs/api/Util.md b/documentation/docs/api/Util.md similarity index 100% rename from docs/api/Util.md rename to documentation/docs/api/Util.md diff --git a/docs/api/WebSocket.md b/documentation/docs/api/WebSocket.md similarity index 100% rename from docs/api/WebSocket.md rename to documentation/docs/api/WebSocket.md diff --git a/docs/api/api-lifecycle.md b/documentation/docs/api/api-lifecycle.md similarity index 100% rename from docs/api/api-lifecycle.md rename to documentation/docs/api/api-lifecycle.md diff --git a/docs/best-practices/client-certificate.md b/documentation/docs/best-practices/client-certificate.md similarity index 100% rename from docs/best-practices/client-certificate.md rename to documentation/docs/best-practices/client-certificate.md diff --git a/docs/best-practices/mocking-request.md b/documentation/docs/best-practices/mocking-request.md similarity index 100% rename from docs/best-practices/mocking-request.md rename to documentation/docs/best-practices/mocking-request.md diff --git a/docs/best-practices/proxy.md b/documentation/docs/best-practices/proxy.md similarity index 100% rename from docs/best-practices/proxy.md rename to documentation/docs/best-practices/proxy.md diff --git a/docs/best-practices/writing-tests.md b/documentation/docs/best-practices/writing-tests.md similarity index 100% rename from docs/best-practices/writing-tests.md rename to documentation/docs/best-practices/writing-tests.md diff --git a/docsify/sidebar.md b/documentation/docsify/sidebar.md similarity index 100% rename from docsify/sidebar.md rename to documentation/docsify/sidebar.md diff --git a/examples/README.md b/documentation/examples/README.md similarity index 100% rename from examples/README.md rename to documentation/examples/README.md diff --git a/examples/ca-fingerprint/index.js b/documentation/examples/ca-fingerprint/index.js similarity index 97% rename from examples/ca-fingerprint/index.js rename to documentation/examples/ca-fingerprint/index.js index 7fa6a5ac0fa..b4dfc41124c 100644 --- a/examples/ca-fingerprint/index.js +++ b/documentation/examples/ca-fingerprint/index.js @@ -2,7 +2,7 @@ const crypto = require('node:crypto') const https = require('node:https') -const { Client, buildConnector } = require('../..') +const { Client, buildConnector } = require('../../../') const pem = require('https-pem') const caFingerprint = getFingerprint(pem.cert.toString() diff --git a/examples/eventsource.js b/documentation/examples/eventsource.js similarity index 92% rename from examples/eventsource.js rename to documentation/examples/eventsource.js index ca280cb4129..3aa82556f19 100644 --- a/examples/eventsource.js +++ b/documentation/examples/eventsource.js @@ -1,7 +1,7 @@ 'use strict' const { randomBytes } = require('node:crypto') -const { EventSource } = require('../') +const { EventSource } = require('../../') async function main () { const url = `https://smee.io/${randomBytes(8).toString('base64url')}` diff --git a/examples/fetch.js b/documentation/examples/fetch.js similarity index 87% rename from examples/fetch.js rename to documentation/examples/fetch.js index 7ece2b84083..26f32ebe418 100644 --- a/examples/fetch.js +++ b/documentation/examples/fetch.js @@ -1,6 +1,6 @@ 'use strict' -const { fetch } = require('../') +const { fetch } = require('../../') async function main () { const res = await fetch('http://localhost:3001/') diff --git a/examples/proxy-agent.js b/documentation/examples/proxy-agent.js similarity index 98% rename from examples/proxy-agent.js rename to documentation/examples/proxy-agent.js index 7caf836d7f1..df41c615be8 100644 --- a/examples/proxy-agent.js +++ b/documentation/examples/proxy-agent.js @@ -1,6 +1,6 @@ 'use strict' -const { request, setGlobalDispatcher, ProxyAgent } = require('../') +const { request, setGlobalDispatcher, ProxyAgent } = require('../..') setGlobalDispatcher(new ProxyAgent('http://localhost:8000/')) diff --git a/examples/proxy/index.js b/documentation/examples/proxy/index.js similarity index 95% rename from examples/proxy/index.js rename to documentation/examples/proxy/index.js index 16aeaeeaba3..eeb8c330b38 100644 --- a/examples/proxy/index.js +++ b/documentation/examples/proxy/index.js @@ -1,4 +1,4 @@ -const { Pool, Client } = require('../../') +const { Pool, Client } = require('../../../') const http = require('node:http') const proxy = require('./proxy') diff --git a/examples/proxy/proxy.js b/documentation/examples/proxy/proxy.js similarity index 100% rename from examples/proxy/proxy.js rename to documentation/examples/proxy/proxy.js diff --git a/examples/proxy/websocket.js b/documentation/examples/proxy/websocket.js similarity index 97% rename from examples/proxy/websocket.js rename to documentation/examples/proxy/websocket.js index f21cafdc838..e71d8f0768c 100644 --- a/examples/proxy/websocket.js +++ b/documentation/examples/proxy/websocket.js @@ -1,4 +1,4 @@ -const { Pool, Client } = require('../../') +const { Pool, Client } = require('../../../') const http = require('node:http') const proxy = require('./proxy') const WebSocket = require('ws') diff --git a/examples/request.js b/documentation/examples/request.js similarity index 98% rename from examples/request.js rename to documentation/examples/request.js index aa3a86e849e..f9aba754154 100644 --- a/examples/request.js +++ b/documentation/examples/request.js @@ -1,6 +1,6 @@ 'use strict' -const { request } = require('../') +const { request } = require('../../') async function getRequest (port = 3001) { // A simple GET request diff --git a/index.html b/documentation/index.html similarity index 100% rename from index.html rename to documentation/index.html diff --git a/documentation/package.json b/documentation/package.json new file mode 100644 index 00000000000..491fcfc4f6c --- /dev/null +++ b/documentation/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "@undici/documentation", + "description": "Documentation site for the `undici` package.", + "scripts": { + "serve": "docsify serve ." + }, + "dependencies": { + "docsify-cli": "^4.4.4" + } +} diff --git a/package.json b/package.json index fabb4d5f1ba..47c7000480c 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "bench:server": "node benchmarks/server.js", "prebench:run": "node benchmarks/wait.js", "bench:run": "SAMPLES=100 CONNECTIONS=50 node benchmarks/benchmark.js", - "serve:website": "docsify serve .", + "serve:website": "echo \"Error: Documentation has been moved to '\/documentation'\" && exit 1", "prepare": "husky install", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, @@ -101,7 +101,6 @@ "concurrently": "^8.0.1", "cronometro": "^3.0.1", "dns-packet": "^5.4.0", - "docsify-cli": "^4.4.3", "form-data": "^4.0.0", "formdata-node": "^6.0.3", "got": "^14.0.0", diff --git a/test/examples.js b/test/examples.js index d344236b68d..5603cb3faaf 100644 --- a/test/examples.js +++ b/test/examples.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') const { test, after } = require('node:test') const { once } = require('node:events') -const examples = require('../examples/request.js') +const examples = require('../documentation/examples/request.js') test('request examples', async (t) => { t = tspl(t, { plan: 7 }) From ac9b6c63cf5d03e64c8167567a29723a417c548d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:50:01 +0000 Subject: [PATCH 059/123] build(deps): bump fastify/github-action-merge-dependabot (#2820) Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 3.9.1 to 3.10.1. - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases) - [Commits](https://github.com/fastify/github-action-merge-dependabot/compare/59fc8817458fac20df8884576cfe69dbb77c9a07...9e7bfb249c69139d7bdcd8d984f9665edd49020b) --- updated-dependencies: - dependency-name: fastify/github-action-merge-dependabot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index df5a0297da7..8d1f63e9398 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -33,6 +33,6 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@59fc8817458fac20df8884576cfe69dbb77c9a07 # v3.9.1 + - uses: fastify/github-action-merge-dependabot@9e7bfb249c69139d7bdcd8d984f9665edd49020b # v3.10.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 08f07e93b77c458dd32aa971299632121202adc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:50:40 +0000 Subject: [PATCH 060/123] build(deps): bump actions/dependency-review-action from 4.0.0 to 4.1.3 (#2821) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.0.0 to 4.1.3. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/4901385134134e04cec5fbe5ddfe3b2c5bd5d976...9129d7d40b8c12c1ed0f60400d00c92d437adcce) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 74f249a0c77..55928d96318 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: 'Dependency Review' - uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 + uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 From fea7d34326c1aba28d6295aa2d9cd6f662a96202 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:50:49 +0000 Subject: [PATCH 061/123] build(deps): bump github/codeql-action from 3.23.2 to 3.24.4 (#2818) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.23.2 to 3.24.4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b7bf0a3ed3ecfa44160715d7c442788f65f0f923...e2e140ad1441662206e8f97754b166877dfa1c73) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ef05b49ee0d..505bae5f8ce 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v2.3.3 + uses: github/codeql-action/init@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v2.3.3 + uses: github/codeql-action/autobuild@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v2.3.3 + uses: github/codeql-action/analyze@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c92e564fa4f..623a220c74a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/upload-sarif@e2e140ad1441662206e8f97754b166877dfa1c73 # v3.24.4 with: sarif_file: results.sarif From b0920fa348372a8f320800ec2bd25cdb27f8d83a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:50:54 +0000 Subject: [PATCH 062/123] build(deps): bump actions/setup-node from 4.0.1 to 4.0.2 (#2819) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8...60edb5dd545a775178f52524783378180af0d1f8) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/publish-undici-types.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 9cf58db98bd..545f5d9d081 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -17,7 +17,7 @@ jobs: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* - name: Install Modules @@ -34,7 +34,7 @@ jobs: with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* - name: Install Modules diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 87975c98338..d86863210fa 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -16,7 +16,7 @@ jobs: persist-credentials: false - name: Setup Node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 49fd98ea6ad..06b1ab1cca2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* - run: npm install diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index efd62556aa0..2d56f755231 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '16.x' registry-url: 'https://registry.npmjs.org' From 85a24027773c02aaa55256a64a95f652de0cd324 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 23 Feb 2024 01:50:11 +0100 Subject: [PATCH 063/123] fix: move CNAME and .nojekyll to root (#2822) --- documentation/.nojekyll => .nojekyll | 0 documentation/CNAME => CNAME | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename documentation/.nojekyll => .nojekyll (100%) rename documentation/CNAME => CNAME (100%) diff --git a/documentation/.nojekyll b/.nojekyll similarity index 100% rename from documentation/.nojekyll rename to .nojekyll diff --git a/documentation/CNAME b/CNAME similarity index 100% rename from documentation/CNAME rename to CNAME From 2a1721955a40ff89b4388c445998ff3353718e7a Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Thu, 22 Feb 2024 16:55:53 -0800 Subject: [PATCH 064/123] fix docs --- .github/dependabot.yml | 2 +- .npmignore | 2 +- README.md | 2 +- .nojekyll => docs/.nojekyll | 0 CNAME => docs/CNAME | 0 {documentation => docs}/README.md | 0 {documentation => docs}/docs/api/Agent.md | 0 {documentation => docs}/docs/api/BalancedPool.md | 0 {documentation => docs}/docs/api/CacheStorage.md | 0 {documentation => docs}/docs/api/Client.md | 0 {documentation => docs}/docs/api/Connector.md | 0 {documentation => docs}/docs/api/ContentType.md | 0 {documentation => docs}/docs/api/Cookies.md | 0 {documentation => docs}/docs/api/Debug.md | 0 {documentation => docs}/docs/api/DiagnosticsChannel.md | 0 {documentation => docs}/docs/api/DispatchInterceptor.md | 0 {documentation => docs}/docs/api/Dispatcher.md | 0 {documentation => docs}/docs/api/Errors.md | 0 {documentation => docs}/docs/api/EventSource.md | 0 {documentation => docs}/docs/api/Fetch.md | 0 {documentation => docs}/docs/api/MockAgent.md | 0 {documentation => docs}/docs/api/MockClient.md | 0 {documentation => docs}/docs/api/MockErrors.md | 0 {documentation => docs}/docs/api/MockPool.md | 0 {documentation => docs}/docs/api/Pool.md | 0 {documentation => docs}/docs/api/PoolStats.md | 0 {documentation => docs}/docs/api/ProxyAgent.md | 0 {documentation => docs}/docs/api/RedirectHandler.md | 0 {documentation => docs}/docs/api/RetryAgent.md | 0 {documentation => docs}/docs/api/RetryHandler.md | 0 {documentation => docs}/docs/api/Util.md | 0 {documentation => docs}/docs/api/WebSocket.md | 0 {documentation => docs}/docs/api/api-lifecycle.md | 0 .../docs/best-practices/client-certificate.md | 0 {documentation => docs}/docs/best-practices/mocking-request.md | 0 {documentation => docs}/docs/best-practices/proxy.md | 0 {documentation => docs}/docs/best-practices/writing-tests.md | 0 {documentation => docs}/docsify/sidebar.md | 0 {documentation => docs}/examples/README.md | 0 {documentation => docs}/examples/ca-fingerprint/index.js | 0 {documentation => docs}/examples/eventsource.js | 0 {documentation => docs}/examples/fetch.js | 0 {documentation => docs}/examples/proxy-agent.js | 0 {documentation => docs}/examples/proxy/index.js | 0 {documentation => docs}/examples/proxy/proxy.js | 0 {documentation => docs}/examples/proxy/websocket.js | 0 {documentation => docs}/examples/request.js | 0 {documentation => docs}/index.html | 0 {documentation => docs}/package.json | 0 package.json | 2 +- test/examples.js | 2 +- 51 files changed, 5 insertions(+), 5 deletions(-) rename .nojekyll => docs/.nojekyll (100%) rename CNAME => docs/CNAME (100%) rename {documentation => docs}/README.md (100%) rename {documentation => docs}/docs/api/Agent.md (100%) rename {documentation => docs}/docs/api/BalancedPool.md (100%) rename {documentation => docs}/docs/api/CacheStorage.md (100%) rename {documentation => docs}/docs/api/Client.md (100%) rename {documentation => docs}/docs/api/Connector.md (100%) rename {documentation => docs}/docs/api/ContentType.md (100%) rename {documentation => docs}/docs/api/Cookies.md (100%) rename {documentation => docs}/docs/api/Debug.md (100%) rename {documentation => docs}/docs/api/DiagnosticsChannel.md (100%) rename {documentation => docs}/docs/api/DispatchInterceptor.md (100%) rename {documentation => docs}/docs/api/Dispatcher.md (100%) rename {documentation => docs}/docs/api/Errors.md (100%) rename {documentation => docs}/docs/api/EventSource.md (100%) rename {documentation => docs}/docs/api/Fetch.md (100%) rename {documentation => docs}/docs/api/MockAgent.md (100%) rename {documentation => docs}/docs/api/MockClient.md (100%) rename {documentation => docs}/docs/api/MockErrors.md (100%) rename {documentation => docs}/docs/api/MockPool.md (100%) rename {documentation => docs}/docs/api/Pool.md (100%) rename {documentation => docs}/docs/api/PoolStats.md (100%) rename {documentation => docs}/docs/api/ProxyAgent.md (100%) rename {documentation => docs}/docs/api/RedirectHandler.md (100%) rename {documentation => docs}/docs/api/RetryAgent.md (100%) rename {documentation => docs}/docs/api/RetryHandler.md (100%) rename {documentation => docs}/docs/api/Util.md (100%) rename {documentation => docs}/docs/api/WebSocket.md (100%) rename {documentation => docs}/docs/api/api-lifecycle.md (100%) rename {documentation => docs}/docs/best-practices/client-certificate.md (100%) rename {documentation => docs}/docs/best-practices/mocking-request.md (100%) rename {documentation => docs}/docs/best-practices/proxy.md (100%) rename {documentation => docs}/docs/best-practices/writing-tests.md (100%) rename {documentation => docs}/docsify/sidebar.md (100%) rename {documentation => docs}/examples/README.md (100%) rename {documentation => docs}/examples/ca-fingerprint/index.js (100%) rename {documentation => docs}/examples/eventsource.js (100%) rename {documentation => docs}/examples/fetch.js (100%) rename {documentation => docs}/examples/proxy-agent.js (100%) rename {documentation => docs}/examples/proxy/index.js (100%) rename {documentation => docs}/examples/proxy/proxy.js (100%) rename {documentation => docs}/examples/proxy/websocket.js (100%) rename {documentation => docs}/examples/request.js (100%) rename {documentation => docs}/index.html (100%) rename {documentation => docs}/package.json (100%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec90956a8ea..49b8cc8a17a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,7 @@ updates: open-pull-requests-limit: 10 - package-ecosystem: "npm" - directory: /documentation + directory: /docs schedule: interval: "weekly" open-pull-requests-limit: 10 diff --git a/.npmignore b/.npmignore index a00ceea145e..003eb6c62ff 100644 --- a/.npmignore +++ b/.npmignore @@ -9,4 +9,4 @@ lib/llhttp/llhttp.wasm !types/**/* !index.d.ts -!documentation/docs/**/* +!docs/docs/**/* diff --git a/README.md b/README.md index c95b8a6a153..0e01b4308c1 120000 --- a/README.md +++ b/README.md @@ -1 +1 @@ -documentation/README.md \ No newline at end of file +docs/README.md \ No newline at end of file diff --git a/.nojekyll b/docs/.nojekyll similarity index 100% rename from .nojekyll rename to docs/.nojekyll diff --git a/CNAME b/docs/CNAME similarity index 100% rename from CNAME rename to docs/CNAME diff --git a/documentation/README.md b/docs/README.md similarity index 100% rename from documentation/README.md rename to docs/README.md diff --git a/documentation/docs/api/Agent.md b/docs/docs/api/Agent.md similarity index 100% rename from documentation/docs/api/Agent.md rename to docs/docs/api/Agent.md diff --git a/documentation/docs/api/BalancedPool.md b/docs/docs/api/BalancedPool.md similarity index 100% rename from documentation/docs/api/BalancedPool.md rename to docs/docs/api/BalancedPool.md diff --git a/documentation/docs/api/CacheStorage.md b/docs/docs/api/CacheStorage.md similarity index 100% rename from documentation/docs/api/CacheStorage.md rename to docs/docs/api/CacheStorage.md diff --git a/documentation/docs/api/Client.md b/docs/docs/api/Client.md similarity index 100% rename from documentation/docs/api/Client.md rename to docs/docs/api/Client.md diff --git a/documentation/docs/api/Connector.md b/docs/docs/api/Connector.md similarity index 100% rename from documentation/docs/api/Connector.md rename to docs/docs/api/Connector.md diff --git a/documentation/docs/api/ContentType.md b/docs/docs/api/ContentType.md similarity index 100% rename from documentation/docs/api/ContentType.md rename to docs/docs/api/ContentType.md diff --git a/documentation/docs/api/Cookies.md b/docs/docs/api/Cookies.md similarity index 100% rename from documentation/docs/api/Cookies.md rename to docs/docs/api/Cookies.md diff --git a/documentation/docs/api/Debug.md b/docs/docs/api/Debug.md similarity index 100% rename from documentation/docs/api/Debug.md rename to docs/docs/api/Debug.md diff --git a/documentation/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md similarity index 100% rename from documentation/docs/api/DiagnosticsChannel.md rename to docs/docs/api/DiagnosticsChannel.md diff --git a/documentation/docs/api/DispatchInterceptor.md b/docs/docs/api/DispatchInterceptor.md similarity index 100% rename from documentation/docs/api/DispatchInterceptor.md rename to docs/docs/api/DispatchInterceptor.md diff --git a/documentation/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md similarity index 100% rename from documentation/docs/api/Dispatcher.md rename to docs/docs/api/Dispatcher.md diff --git a/documentation/docs/api/Errors.md b/docs/docs/api/Errors.md similarity index 100% rename from documentation/docs/api/Errors.md rename to docs/docs/api/Errors.md diff --git a/documentation/docs/api/EventSource.md b/docs/docs/api/EventSource.md similarity index 100% rename from documentation/docs/api/EventSource.md rename to docs/docs/api/EventSource.md diff --git a/documentation/docs/api/Fetch.md b/docs/docs/api/Fetch.md similarity index 100% rename from documentation/docs/api/Fetch.md rename to docs/docs/api/Fetch.md diff --git a/documentation/docs/api/MockAgent.md b/docs/docs/api/MockAgent.md similarity index 100% rename from documentation/docs/api/MockAgent.md rename to docs/docs/api/MockAgent.md diff --git a/documentation/docs/api/MockClient.md b/docs/docs/api/MockClient.md similarity index 100% rename from documentation/docs/api/MockClient.md rename to docs/docs/api/MockClient.md diff --git a/documentation/docs/api/MockErrors.md b/docs/docs/api/MockErrors.md similarity index 100% rename from documentation/docs/api/MockErrors.md rename to docs/docs/api/MockErrors.md diff --git a/documentation/docs/api/MockPool.md b/docs/docs/api/MockPool.md similarity index 100% rename from documentation/docs/api/MockPool.md rename to docs/docs/api/MockPool.md diff --git a/documentation/docs/api/Pool.md b/docs/docs/api/Pool.md similarity index 100% rename from documentation/docs/api/Pool.md rename to docs/docs/api/Pool.md diff --git a/documentation/docs/api/PoolStats.md b/docs/docs/api/PoolStats.md similarity index 100% rename from documentation/docs/api/PoolStats.md rename to docs/docs/api/PoolStats.md diff --git a/documentation/docs/api/ProxyAgent.md b/docs/docs/api/ProxyAgent.md similarity index 100% rename from documentation/docs/api/ProxyAgent.md rename to docs/docs/api/ProxyAgent.md diff --git a/documentation/docs/api/RedirectHandler.md b/docs/docs/api/RedirectHandler.md similarity index 100% rename from documentation/docs/api/RedirectHandler.md rename to docs/docs/api/RedirectHandler.md diff --git a/documentation/docs/api/RetryAgent.md b/docs/docs/api/RetryAgent.md similarity index 100% rename from documentation/docs/api/RetryAgent.md rename to docs/docs/api/RetryAgent.md diff --git a/documentation/docs/api/RetryHandler.md b/docs/docs/api/RetryHandler.md similarity index 100% rename from documentation/docs/api/RetryHandler.md rename to docs/docs/api/RetryHandler.md diff --git a/documentation/docs/api/Util.md b/docs/docs/api/Util.md similarity index 100% rename from documentation/docs/api/Util.md rename to docs/docs/api/Util.md diff --git a/documentation/docs/api/WebSocket.md b/docs/docs/api/WebSocket.md similarity index 100% rename from documentation/docs/api/WebSocket.md rename to docs/docs/api/WebSocket.md diff --git a/documentation/docs/api/api-lifecycle.md b/docs/docs/api/api-lifecycle.md similarity index 100% rename from documentation/docs/api/api-lifecycle.md rename to docs/docs/api/api-lifecycle.md diff --git a/documentation/docs/best-practices/client-certificate.md b/docs/docs/best-practices/client-certificate.md similarity index 100% rename from documentation/docs/best-practices/client-certificate.md rename to docs/docs/best-practices/client-certificate.md diff --git a/documentation/docs/best-practices/mocking-request.md b/docs/docs/best-practices/mocking-request.md similarity index 100% rename from documentation/docs/best-practices/mocking-request.md rename to docs/docs/best-practices/mocking-request.md diff --git a/documentation/docs/best-practices/proxy.md b/docs/docs/best-practices/proxy.md similarity index 100% rename from documentation/docs/best-practices/proxy.md rename to docs/docs/best-practices/proxy.md diff --git a/documentation/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md similarity index 100% rename from documentation/docs/best-practices/writing-tests.md rename to docs/docs/best-practices/writing-tests.md diff --git a/documentation/docsify/sidebar.md b/docs/docsify/sidebar.md similarity index 100% rename from documentation/docsify/sidebar.md rename to docs/docsify/sidebar.md diff --git a/documentation/examples/README.md b/docs/examples/README.md similarity index 100% rename from documentation/examples/README.md rename to docs/examples/README.md diff --git a/documentation/examples/ca-fingerprint/index.js b/docs/examples/ca-fingerprint/index.js similarity index 100% rename from documentation/examples/ca-fingerprint/index.js rename to docs/examples/ca-fingerprint/index.js diff --git a/documentation/examples/eventsource.js b/docs/examples/eventsource.js similarity index 100% rename from documentation/examples/eventsource.js rename to docs/examples/eventsource.js diff --git a/documentation/examples/fetch.js b/docs/examples/fetch.js similarity index 100% rename from documentation/examples/fetch.js rename to docs/examples/fetch.js diff --git a/documentation/examples/proxy-agent.js b/docs/examples/proxy-agent.js similarity index 100% rename from documentation/examples/proxy-agent.js rename to docs/examples/proxy-agent.js diff --git a/documentation/examples/proxy/index.js b/docs/examples/proxy/index.js similarity index 100% rename from documentation/examples/proxy/index.js rename to docs/examples/proxy/index.js diff --git a/documentation/examples/proxy/proxy.js b/docs/examples/proxy/proxy.js similarity index 100% rename from documentation/examples/proxy/proxy.js rename to docs/examples/proxy/proxy.js diff --git a/documentation/examples/proxy/websocket.js b/docs/examples/proxy/websocket.js similarity index 100% rename from documentation/examples/proxy/websocket.js rename to docs/examples/proxy/websocket.js diff --git a/documentation/examples/request.js b/docs/examples/request.js similarity index 100% rename from documentation/examples/request.js rename to docs/examples/request.js diff --git a/documentation/index.html b/docs/index.html similarity index 100% rename from documentation/index.html rename to docs/index.html diff --git a/documentation/package.json b/docs/package.json similarity index 100% rename from documentation/package.json rename to docs/package.json diff --git a/package.json b/package.json index 47c7000480c..748b8098ca0 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "bench:server": "node benchmarks/server.js", "prebench:run": "node benchmarks/wait.js", "bench:run": "SAMPLES=100 CONNECTIONS=50 node benchmarks/benchmark.js", - "serve:website": "echo \"Error: Documentation has been moved to '\/documentation'\" && exit 1", + "serve:website": "echo \"Error: Documentation has been moved to '\/docs'\" && exit 1", "prepare": "husky install", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, diff --git a/test/examples.js b/test/examples.js index 5603cb3faaf..e06b2d4ddb8 100644 --- a/test/examples.js +++ b/test/examples.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') const { test, after } = require('node:test') const { once } = require('node:events') -const examples = require('../documentation/examples/request.js') +const examples = require('../docs/examples/request.js') test('request examples', async (t) => { t = tspl(t, { plan: 7 }) From 3274c975947ce11a08508743df026f73598bfead Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 23 Feb 2024 02:15:32 -0500 Subject: [PATCH 065/123] remove all fetchParam event handlers (#2823) --- lib/web/fetch/index.js | 11 +++++------ test/fetch/issue-1711.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 test/fetch/issue-1711.js diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index f4a6e5e6262..df2f442c647 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -81,12 +81,6 @@ class Fetch extends EE { this.connection = null this.dump = false this.state = 'ongoing' - // 2 terminated listeners get added per request, - // but only 1 gets removed. If there are 20 redirects, - // 21 listeners will be added. - // See https://github.com/nodejs/undici/issues/1711 - // TODO (fix): Find and fix root cause for leaked listener. - this.setMaxListeners(21) } terminate (reason) { @@ -1967,6 +1961,7 @@ async function httpNetworkFetch ( // 19. Run these steps in parallel: // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.onAborted = onAborted fetchParams.controller.on('terminated', onAborted) fetchParams.controller.resume = async () => { // 1. While true @@ -2235,6 +2230,10 @@ async function httpNetworkFetch ( fetchParams.controller.off('terminated', this.abort) } + if (fetchParams.controller.onAborted) { + fetchParams.controller.off('terminated', fetchParams.controller.onAborted) + } + fetchParams.controller.ended = true this.body.push(null) diff --git a/test/fetch/issue-1711.js b/test/fetch/issue-1711.js new file mode 100644 index 00000000000..b024e411195 --- /dev/null +++ b/test/fetch/issue-1711.js @@ -0,0 +1,33 @@ +'use strict' + +const assert = require('node:assert') +const { once } = require('node:events') +const { createServer } = require('node:http') +const { test } = require('node:test') +const { fetch } = require('../..') + +test('Redirecting a bunch does not cause a MaxListenersExceededWarning', async (t) => { + let redirects = 0 + + const server = createServer((req, res) => { + if (redirects === 15) { + res.end('Okay goodbye') + return + } + + res.writeHead(302, { + Location: `/${redirects++}` + }) + res.end() + }).listen(0) + + t.after(server.close.bind(server)) + await once(server, 'listening') + + process.emitWarning = assert.bind(null, false) + + const url = `http://localhost:${server.address().port}` + const response = await fetch(url, { redirect: 'follow' }) + + assert.deepStrictEqual(response.url, `${url}/${redirects - 1}`) +}) From d830c94993f62155a1fc851fe417406f9a2e316e Mon Sep 17 00:00:00 2001 From: Lorenzo Rossi <65499789+rossilor95@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:07:29 +0100 Subject: [PATCH 066/123] feat: refactor ProxyAgent constructor to also accept single URL argument (#2810) * feat: add support for opts as URL in ProxyAgent * test: update ProxyAgent unit tests * docs: update ProxyAgent documentation --- docs/docs/api/ProxyAgent.md | 4 ++- lib/proxy-agent.js | 58 +++++++++++++++++-------------------- test/proxy-agent.js | 46 +++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/docs/docs/api/ProxyAgent.md b/docs/docs/api/ProxyAgent.md index cebfe689f39..eaa6329f473 100644 --- a/docs/docs/api/ProxyAgent.md +++ b/docs/docs/api/ProxyAgent.md @@ -16,7 +16,7 @@ Returns: `ProxyAgent` Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) -* **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string. +* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string. * **token** `string` (optional) - It can be passed by a string of token for authentication. * **auth** `string` (**deprecated**) - Use token. * **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)` @@ -30,6 +30,8 @@ import { ProxyAgent } from 'undici' const proxyAgent = new ProxyAgent('my.proxy.server') // or +const proxyAgent = new ProxyAgent(new URL('my.proxy.server')) +// or const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' }) ``` diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js index 382be2a5940..c80134b742d 100644 --- a/lib/proxy-agent.js +++ b/lib/proxy-agent.js @@ -19,55 +19,35 @@ function defaultProtocolPort (protocol) { return protocol === 'https:' ? 443 : 80 } -function buildProxyOptions (opts) { - if (typeof opts === 'string') { - opts = { uri: opts } - } - - if (!opts || !opts.uri) { - throw new InvalidArgumentError('Proxy opts.uri is mandatory') - } - - return { - uri: opts.uri, - protocol: opts.protocol || 'https' - } -} - function defaultFactory (origin, opts) { return new Pool(origin, opts) } class ProxyAgent extends DispatcherBase { constructor (opts) { - super(opts) - this[kProxy] = buildProxyOptions(opts) - this[kAgent] = new Agent(opts) - this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) - ? opts.interceptors.ProxyAgent - : [] + super() - if (typeof opts === 'string') { - opts = { uri: opts } - } - - if (!opts || !opts.uri) { - throw new InvalidArgumentError('Proxy opts.uri is mandatory') + if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) { + throw new InvalidArgumentError('Proxy uri is mandatory') } const { clientFactory = defaultFactory } = opts - if (typeof clientFactory !== 'function') { throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') } + const url = this.#getUrl(opts) + const { href, origin, port, protocol, username, password } = url + + this[kProxy] = { uri: href, protocol } + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] this[kRequestTls] = opts.requestTls this[kProxyTls] = opts.proxyTls this[kProxyHeaders] = opts.headers || {} - const resolvedUrl = new URL(opts.uri) - const { origin, port, username, password } = resolvedUrl - if (opts.auth && opts.token) { throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') } else if (opts.auth) { @@ -81,7 +61,7 @@ class ProxyAgent extends DispatcherBase { const connect = buildConnector({ ...opts.proxyTls }) this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) - this[kClient] = clientFactory(resolvedUrl, { connect }) + this[kClient] = clientFactory(url, { connect }) this[kAgent] = new Agent({ ...opts, connect: async (opts, callback) => { @@ -138,6 +118,20 @@ class ProxyAgent extends DispatcherBase { ) } + /** + * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @returns {URL} + */ + #getUrl (opts) { + if (typeof opts === 'string') { + return new URL(opts) + } else if (opts instanceof URL) { + return opts + } else { + return new URL(opts.uri) + } + } + async [kClose] () { await this[kAgent].close() await this[kClient].close() diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 87129d01472..8c6634ff1fb 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -29,9 +29,10 @@ test('using auth in combination with token should throw', (t) => { ) }) -test('should accept string and object as options', (t) => { - t = tspl(t, { plan: 2 }) +test('should accept string, URL and object as options', (t) => { + t = tspl(t, { plan: 3 }) t.doesNotThrow(() => new ProxyAgent('http://example.com')) + t.doesNotThrow(() => new ProxyAgent(new URL('http://example.com'))) t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' })) }) @@ -148,6 +149,47 @@ test('use proxy-agent to connect through proxy using path with params', async (t proxyAgent.close() }) +test('use proxy-agent to connect through proxy with basic auth in URL', async (t) => { + t = tspl(t, { plan: 7 }) + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`) + const proxyAgent = new ProxyAgent(proxyUrl) + const parsedOrigin = new URL(serverUrl) + + proxy.authenticate = function (req, fn) { + t.ok(true, 'authentication should be called') + fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) + } + proxy.on('connect', () => { + t.ok(true, 'proxy should be called') + }) + + server.on('request', (req, res) => { + t.strictEqual(req.url, '/hello?foo=bar') + t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host') + res.setHeader('content-type', 'application/json') + res.end(JSON.stringify({ hello: 'world' })) + }) + + const { + statusCode, + headers, + body + } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent }) + const json = await body.json() + + t.strictEqual(statusCode, 200) + t.deepStrictEqual(json, { hello: 'world' }) + t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open') + + server.close() + proxy.close() + proxyAgent.close() +}) + test('use proxy-agent with auth', async (t) => { t = tspl(t, { plan: 7 }) const server = await buildServer() From e7b4714647f8161e8894d258bc8518f8b87d511a Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 23 Feb 2024 11:13:10 +0100 Subject: [PATCH 067/123] fix: isCTLExcludingHtab (#2790) * fix: isCTLExcludingHtab * Update benchmarks/cookiesIsCTLExcludingHtab.mjs Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> * Update lib/cookies/util.js Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> * simplify * fix paths --------- Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- benchmarks/cookiesIsCTLExcludingHtab.mjs | 17 +++++ lib/web/cookies/util.js | 19 +++--- test/cookie/is-ctl-excluding-htab.js | 85 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 benchmarks/cookiesIsCTLExcludingHtab.mjs create mode 100644 test/cookie/is-ctl-excluding-htab.js diff --git a/benchmarks/cookiesIsCTLExcludingHtab.mjs b/benchmarks/cookiesIsCTLExcludingHtab.mjs new file mode 100644 index 00000000000..b0e134b22d2 --- /dev/null +++ b/benchmarks/cookiesIsCTLExcludingHtab.mjs @@ -0,0 +1,17 @@ +import { bench, group, run } from 'mitata' +import { isCTLExcludingHtab } from '../lib/web/cookies/util.js' + +const valid = 'Space=Cat; Secure; HttpOnly; Max-Age=2' +const invalid = 'Space=Cat; Secure; HttpOnly; Max-Age=2\x7F' + +group('isCTLExcludingHtab', () => { + bench(`valid: ${valid}`, () => { + return isCTLExcludingHtab(valid) + }) + + bench(`invalid: ${invalid}`, () => { + return isCTLExcludingHtab(invalid) + }) +}) + +await run() diff --git a/lib/web/cookies/util.js b/lib/web/cookies/util.js index 0c1353d5ca3..6d3a79b69ad 100644 --- a/lib/web/cookies/util.js +++ b/lib/web/cookies/util.js @@ -3,22 +3,23 @@ const assert = require('node:assert') const { kHeadersList } = require('../../core/symbols') +/** + * @param {string} value + * @returns {boolean} + */ function isCTLExcludingHtab (value) { - if (value.length === 0) { - return false - } - - for (const char of value) { - const code = char.charCodeAt(0) + for (let i = 0; i < value.length; ++i) { + const code = value.charCodeAt(i) if ( - (code >= 0x00 || code <= 0x08) || - (code >= 0x0A || code <= 0x1F) || + (code >= 0x00 && code <= 0x08) || + (code >= 0x0A && code <= 0x1F) || code === 0x7F ) { - return false + return true } } + return false } /** diff --git a/test/cookie/is-ctl-excluding-htab.js b/test/cookie/is-ctl-excluding-htab.js new file mode 100644 index 00000000000..a9523326553 --- /dev/null +++ b/test/cookie/is-ctl-excluding-htab.js @@ -0,0 +1,85 @@ +'use strict' + +const { test, describe } = require('node:test') +const { strictEqual } = require('node:assert') + +const { + isCTLExcludingHtab +} = require('../../lib/web/cookies/util') + +describe('isCTLExcludingHtab', () => { + test('should return false for 0x00 - 0x08 characters', () => { + strictEqual(isCTLExcludingHtab('\x00'), true) + strictEqual(isCTLExcludingHtab('\x01'), true) + strictEqual(isCTLExcludingHtab('\x02'), true) + strictEqual(isCTLExcludingHtab('\x03'), true) + strictEqual(isCTLExcludingHtab('\x04'), true) + strictEqual(isCTLExcludingHtab('\x05'), true) + strictEqual(isCTLExcludingHtab('\x06'), true) + strictEqual(isCTLExcludingHtab('\x07'), true) + strictEqual(isCTLExcludingHtab('\x08'), true) + }) + + test('should return false for 0x09 HTAB character', () => { + strictEqual(isCTLExcludingHtab('\x09'), false) + }) + + test('should return false for 0x0A - 0x1F characters', () => { + strictEqual(isCTLExcludingHtab('\x0A'), true) + strictEqual(isCTLExcludingHtab('\x0B'), true) + strictEqual(isCTLExcludingHtab('\x0C'), true) + strictEqual(isCTLExcludingHtab('\x0D'), true) + strictEqual(isCTLExcludingHtab('\x0E'), true) + strictEqual(isCTLExcludingHtab('\x0F'), true) + strictEqual(isCTLExcludingHtab('\x10'), true) + strictEqual(isCTLExcludingHtab('\x11'), true) + strictEqual(isCTLExcludingHtab('\x12'), true) + strictEqual(isCTLExcludingHtab('\x13'), true) + strictEqual(isCTLExcludingHtab('\x14'), true) + strictEqual(isCTLExcludingHtab('\x15'), true) + strictEqual(isCTLExcludingHtab('\x16'), true) + strictEqual(isCTLExcludingHtab('\x17'), true) + strictEqual(isCTLExcludingHtab('\x18'), true) + strictEqual(isCTLExcludingHtab('\x19'), true) + strictEqual(isCTLExcludingHtab('\x1A'), true) + strictEqual(isCTLExcludingHtab('\x1B'), true) + strictEqual(isCTLExcludingHtab('\x1C'), true) + strictEqual(isCTLExcludingHtab('\x1D'), true) + strictEqual(isCTLExcludingHtab('\x1E'), true) + strictEqual(isCTLExcludingHtab('\x1F'), true) + }) + + test('should return false for a 0x7F character', t => { + strictEqual(isCTLExcludingHtab('\x7F'), true) + }) + + test('should return false for a 0x20 / space character', t => { + strictEqual(isCTLExcludingHtab(' '), false) + }) + + test('should return false for a printable character', t => { + strictEqual(isCTLExcludingHtab('A'), false) + strictEqual(isCTLExcludingHtab('Z'), false) + strictEqual(isCTLExcludingHtab('a'), false) + strictEqual(isCTLExcludingHtab('z'), false) + strictEqual(isCTLExcludingHtab('!'), false) + }) + + test('should return false for an empty string', () => { + strictEqual(isCTLExcludingHtab(''), false) + }) + + test('all printable characters (0x20 - 0x7E)', () => { + for (let i = 0x20; i < 0x7F; i++) { + strictEqual(isCTLExcludingHtab(String.fromCharCode(i)), false) + } + }) + + test('valid case', () => { + strictEqual(isCTLExcludingHtab('Space=Cat; Secure; HttpOnly; Max-Age=2'), false) + }) + + test('invalid case', () => { + strictEqual(isCTLExcludingHtab('Space=Cat; Secure; HttpOnly; Max-Age=2\x7F'), true) + }) +}) From cfa2b4e4ec52b6f0b5c6ee231e35ee60bac2789f Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 23 Feb 2024 11:53:31 +0100 Subject: [PATCH 068/123] refactor: move files into logical folders (#2813) --- index.js | 14 ++++++------ lib/{ => dispatcher}/agent.js | 8 +++---- lib/{ => dispatcher}/balanced-pool.js | 6 ++--- lib/{ => dispatcher}/client.js | 30 ++++++++++++------------- lib/{ => dispatcher}/dispatcher-base.js | 4 ++-- lib/{ => dispatcher}/dispatcher.js | 0 lib/{ => dispatcher}/pool-base.js | 4 ++-- lib/{ => dispatcher}/pool-stats.js | 2 +- lib/{ => dispatcher}/pool.js | 8 +++---- lib/{ => dispatcher}/proxy-agent.js | 6 ++--- lib/{ => dispatcher}/retry-agent.js | 2 +- lib/global.js | 2 +- lib/mock/mock-agent.js | 4 ++-- lib/mock/mock-client.js | 2 +- lib/mock/mock-pool.js | 2 +- lib/{ => util}/timers.js | 0 test/client-keep-alive.js | 2 +- test/client-reconnect.js | 2 +- test/client-timeout.js | 2 +- test/dispatcher.js | 2 +- test/fetch/fetch-timeouts.js | 2 +- test/mock-agent.js | 2 +- test/mock-client.js | 2 +- test/mock-pool.js | 2 +- test/pool.js | 2 +- test/proxy-agent.js | 4 ++-- test/request-timeout.js | 2 +- test/socket-timeout.js | 2 +- 28 files changed, 60 insertions(+), 60 deletions(-) rename lib/{ => dispatcher}/agent.js (94%) rename lib/{ => dispatcher}/balanced-pool.js (97%) rename lib/{ => dispatcher}/client.js (98%) rename lib/{ => dispatcher}/dispatcher-base.js (98%) rename lib/{ => dispatcher}/dispatcher.js (100%) rename lib/{ => dispatcher}/pool-base.js (97%) rename lib/{ => dispatcher}/pool-stats.js (94%) rename lib/{ => dispatcher}/pool.js (92%) rename lib/{ => dispatcher}/proxy-agent.js (97%) rename lib/{ => dispatcher}/retry-agent.js (91%) rename lib/{ => util}/timers.js (100%) diff --git a/index.js b/index.js index dd52f2e46a1..4c7a979f001 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,13 @@ 'use strict' -const Client = require('./lib/client') -const Dispatcher = require('./lib/dispatcher') +const Client = require('./lib/dispatcher/client') +const Dispatcher = require('./lib/dispatcher/dispatcher') +const Pool = require('./lib/dispatcher/pool') +const BalancedPool = require('./lib/dispatcher/balanced-pool') +const Agent = require('./lib/dispatcher/agent') +const ProxyAgent = require('./lib/dispatcher/proxy-agent') +const RetryAgent = require('./lib/dispatcher/retry-agent') const errors = require('./lib/core/errors') -const Pool = require('./lib/pool') -const BalancedPool = require('./lib/balanced-pool') -const Agent = require('./lib/agent') const util = require('./lib/core/util') const { InvalidArgumentError } = errors const api = require('./lib/api') @@ -14,8 +16,6 @@ const MockClient = require('./lib/mock/mock-client') const MockAgent = require('./lib/mock/mock-agent') const MockPool = require('./lib/mock/mock-pool') const mockErrors = require('./lib/mock/mock-errors') -const ProxyAgent = require('./lib/proxy-agent') -const RetryAgent = require('./lib/retry-agent') const RetryHandler = require('./lib/handler/RetryHandler') const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') const DecoratorHandler = require('./lib/handler/DecoratorHandler') diff --git a/lib/agent.js b/lib/dispatcher/agent.js similarity index 94% rename from lib/agent.js rename to lib/dispatcher/agent.js index 9a07708654a..d91c412382f 100644 --- a/lib/agent.js +++ b/lib/dispatcher/agent.js @@ -1,12 +1,12 @@ 'use strict' -const { InvalidArgumentError } = require('./core/errors') -const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols') +const { InvalidArgumentError } = require('../core/errors') +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('../core/symbols') const DispatcherBase = require('./dispatcher-base') const Pool = require('./pool') const Client = require('./client') -const util = require('./core/util') -const createRedirectInterceptor = require('./interceptor/redirectInterceptor') +const util = require('../core/util') +const createRedirectInterceptor = require('../interceptor/redirectInterceptor') const kOnConnect = Symbol('onConnect') const kOnDisconnect = Symbol('onDisconnect') diff --git a/lib/balanced-pool.js b/lib/dispatcher/balanced-pool.js similarity index 97% rename from lib/balanced-pool.js rename to lib/dispatcher/balanced-pool.js index cf06fcacedc..15a7e7b5879 100644 --- a/lib/balanced-pool.js +++ b/lib/dispatcher/balanced-pool.js @@ -3,7 +3,7 @@ const { BalancedPoolMissingUpstreamError, InvalidArgumentError -} = require('./core/errors') +} = require('../core/errors') const { PoolBase, kClients, @@ -13,8 +13,8 @@ const { kGetDispatcher } = require('./pool-base') const Pool = require('./pool') -const { kUrl, kInterceptors } = require('./core/symbols') -const { parseOrigin } = require('./core/util') +const { kUrl, kInterceptors } = require('../core/symbols') +const { parseOrigin } = require('../core/util') const kFactory = Symbol('factory') const kOptions = Symbol('options') diff --git a/lib/client.js b/lib/dispatcher/client.js similarity index 98% rename from lib/client.js rename to lib/dispatcher/client.js index 7a7260406ef..cb3feabb37b 100644 --- a/lib/client.js +++ b/lib/dispatcher/client.js @@ -8,10 +8,10 @@ const assert = require('node:assert') const net = require('node:net') const http = require('node:http') const { pipeline } = require('node:stream') -const util = require('./core/util') -const { channels } = require('./core/diagnostics') -const timers = require('./timers') -const Request = require('./core/request') +const util = require('../core/util.js') +const { channels } = require('../core/diagnostics.js') +const timers = require('../util/timers.js') +const Request = require('../core/request.js') const DispatcherBase = require('./dispatcher-base') const { RequestContentLengthMismatchError, @@ -26,8 +26,8 @@ const { HTTPParserError, ResponseExceededMaxSizeError, ClientDestroyedError -} = require('./core/errors') -const buildConnector = require('./core/connect') +} = require('../core/errors.js') +const buildConnector = require('../core/connect.js') const { kUrl, kReset, @@ -79,7 +79,7 @@ const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest -} = require('./core/symbols') +} = require('../core/symbols.js') /** @type {import('http2')} */ let http2 @@ -112,13 +112,13 @@ const FastBuffer = Buffer[Symbol.species] const kClosedResolve = Symbol('kClosedResolve') /** - * @type {import('../types/client').default} + * @type {import('../../types/client.js').default} */ class Client extends DispatcherBase { /** * * @param {string|URL} url - * @param {import('../types/client').Client.Options} options + * @param {import('../../types/client.js').Client.Options} options */ constructor (url, { interceptors, @@ -472,16 +472,16 @@ function onHTTP2GoAway (code) { resume(client) } -const constants = require('./llhttp/constants') -const createRedirectInterceptor = require('./interceptor/redirectInterceptor') +const constants = require('../llhttp/constants.js') +const createRedirectInterceptor = require('../interceptor/redirectInterceptor.js') const EMPTY_BUF = Buffer.alloc(0) async function lazyllhttp () { - const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp-wasm.js') : undefined + const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined let mod try { - mod = await WebAssembly.compile(require('./llhttp/llhttp_simd-wasm.js')) + mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) } catch (e) { /* istanbul ignore next */ @@ -489,7 +489,7 @@ async function lazyllhttp () { // being enabled, but the occurring of this other error // * https://github.com/emscripten-core/emscripten/issues/11495 // got me to remove that check to avoid breaking Node 12. - mod = await WebAssembly.compile(llhttpWasmData || require('./llhttp/llhttp-wasm.js')) + mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) } return await WebAssembly.instantiate(mod, { @@ -1500,7 +1500,7 @@ function write (client, request) { if (util.isFormDataLike(body)) { if (!extractBody) { - extractBody = require('./web/fetch/body.js').extractBody + extractBody = require('../web/fetch/body.js').extractBody } const [bodyStream, contentType] = extractBody(body) diff --git a/lib/dispatcher-base.js b/lib/dispatcher/dispatcher-base.js similarity index 98% rename from lib/dispatcher-base.js rename to lib/dispatcher/dispatcher-base.js index 5c0220b5b33..88e1ca48db7 100644 --- a/lib/dispatcher-base.js +++ b/lib/dispatcher/dispatcher-base.js @@ -5,8 +5,8 @@ const { ClientDestroyedError, ClientClosedError, InvalidArgumentError -} = require('./core/errors') -const { kDestroy, kClose, kDispatch, kInterceptors } = require('./core/symbols') +} = require('../core/errors') +const { kDestroy, kClose, kDispatch, kInterceptors } = require('../core/symbols') const kDestroyed = Symbol('destroyed') const kClosed = Symbol('closed') diff --git a/lib/dispatcher.js b/lib/dispatcher/dispatcher.js similarity index 100% rename from lib/dispatcher.js rename to lib/dispatcher/dispatcher.js diff --git a/lib/pool-base.js b/lib/dispatcher/pool-base.js similarity index 97% rename from lib/pool-base.js rename to lib/dispatcher/pool-base.js index 2a909eee083..6c524e2a437 100644 --- a/lib/pool-base.js +++ b/lib/dispatcher/pool-base.js @@ -1,8 +1,8 @@ 'use strict' const DispatcherBase = require('./dispatcher-base') -const FixedQueue = require('./node/fixed-queue') -const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('./core/symbols') +const FixedQueue = require('../node/fixed-queue') +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols') const PoolStats = require('./pool-stats') const kClients = Symbol('clients') diff --git a/lib/pool-stats.js b/lib/dispatcher/pool-stats.js similarity index 94% rename from lib/pool-stats.js rename to lib/dispatcher/pool-stats.js index b4af8aeed5f..8c7e8c9572e 100644 --- a/lib/pool-stats.js +++ b/lib/dispatcher/pool-stats.js @@ -1,4 +1,4 @@ -const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('./core/symbols') +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('../core/symbols') const kPool = Symbol('pool') class PoolStats { diff --git a/lib/pool.js b/lib/dispatcher/pool.js similarity index 92% rename from lib/pool.js rename to lib/dispatcher/pool.js index d74dcca5604..0ba3a2b5f3e 100644 --- a/lib/pool.js +++ b/lib/dispatcher/pool.js @@ -10,10 +10,10 @@ const { const Client = require('./client') const { InvalidArgumentError -} = require('./core/errors') -const util = require('./core/util') -const { kUrl, kInterceptors } = require('./core/symbols') -const buildConnector = require('./core/connect') +} = require('../core/errors') +const util = require('../core/util') +const { kUrl, kInterceptors } = require('../core/symbols') +const buildConnector = require('../core/connect') const kOptions = Symbol('options') const kConnections = Symbol('connections') diff --git a/lib/proxy-agent.js b/lib/dispatcher/proxy-agent.js similarity index 97% rename from lib/proxy-agent.js rename to lib/dispatcher/proxy-agent.js index c80134b742d..9df39edb1aa 100644 --- a/lib/proxy-agent.js +++ b/lib/dispatcher/proxy-agent.js @@ -1,12 +1,12 @@ 'use strict' -const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols') +const { kProxy, kClose, kDestroy, kInterceptors } = require('../core/symbols') const { URL } = require('node:url') const Agent = require('./agent') const Pool = require('./pool') const DispatcherBase = require('./dispatcher-base') -const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') -const buildConnector = require('./core/connect') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') +const buildConnector = require('../core/connect') const kAgent = Symbol('proxy agent') const kClient = Symbol('proxy client') diff --git a/lib/retry-agent.js b/lib/dispatcher/retry-agent.js similarity index 91% rename from lib/retry-agent.js rename to lib/dispatcher/retry-agent.js index 9edb2aa529f..2ca82b0d02c 100644 --- a/lib/retry-agent.js +++ b/lib/dispatcher/retry-agent.js @@ -1,7 +1,7 @@ 'use strict' const Dispatcher = require('./dispatcher') -const RetryHandler = require('./handler/RetryHandler') +const RetryHandler = require('../handler/RetryHandler') class RetryAgent extends Dispatcher { #agent = null diff --git a/lib/global.js b/lib/global.js index 18bfd73cc92..0c7528fa653 100644 --- a/lib/global.js +++ b/lib/global.js @@ -4,7 +4,7 @@ // this version number must be increased to avoid conflicts. const globalDispatcher = Symbol.for('undici.globalDispatcher.1') const { InvalidArgumentError } = require('./core/errors') -const Agent = require('./agent') +const Agent = require('./dispatcher/agent') if (getGlobalDispatcher() === undefined) { setGlobalDispatcher(new Agent()) diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index 3d26a6a65ba..c02ee375e25 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -1,7 +1,7 @@ 'use strict' const { kClients } = require('../core/symbols') -const Agent = require('../agent') +const Agent = require('../dispatcher/agent') const { kAgent, kMockAgentSet, @@ -17,7 +17,7 @@ const MockClient = require('./mock-client') const MockPool = require('./mock-pool') const { matchValue, buildMockOptions } = require('./mock-utils') const { InvalidArgumentError, UndiciError } = require('../core/errors') -const Dispatcher = require('../dispatcher') +const Dispatcher = require('../dispatcher/dispatcher') const Pluralizer = require('./pluralizer') const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') diff --git a/lib/mock/mock-client.js b/lib/mock/mock-client.js index 193f217501d..c375dbd455b 100644 --- a/lib/mock/mock-client.js +++ b/lib/mock/mock-client.js @@ -1,7 +1,7 @@ 'use strict' const { promisify } = require('node:util') -const Client = require('../client') +const Client = require('../dispatcher/client') const { buildMockDispatch } = require('./mock-utils') const { kDispatches, diff --git a/lib/mock/mock-pool.js b/lib/mock/mock-pool.js index 17695e72eba..8b005d72ead 100644 --- a/lib/mock/mock-pool.js +++ b/lib/mock/mock-pool.js @@ -1,7 +1,7 @@ 'use strict' const { promisify } = require('node:util') -const Pool = require('../pool') +const Pool = require('../dispatcher/pool') const { buildMockDispatch } = require('./mock-utils') const { kDispatches, diff --git a/lib/timers.js b/lib/util/timers.js similarity index 100% rename from lib/timers.js rename to lib/util/timers.js diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index b521624caec..f00e4d8a128 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') -const timers = require('../lib/timers') +const timers = require('../lib/util/timers') const { kConnect } = require('../lib/core/symbols') const { createServer } = require('node:net') const http = require('node:http') diff --git a/test/client-reconnect.js b/test/client-reconnect.js index 34355ce40ac..222922a5ea0 100644 --- a/test/client-reconnect.js +++ b/test/client-reconnect.js @@ -6,7 +6,7 @@ const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') -const timers = require('../lib/timers') +const timers = require('../lib/util/timers') test('multiple reconnect', async (t) => { t = tspl(t, { plan: 5 }) diff --git a/test/client-timeout.js b/test/client-timeout.js index 4964774d0bc..c4ff9c2a59c 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -6,7 +6,7 @@ const { Client, errors } = require('..') const { createServer } = require('node:http') const { Readable } = require('node:stream') const FakeTimers = require('@sinonjs/fake-timers') -const timers = require('../lib/timers') +const timers = require('../lib/util/timers') test('refresh timeout on pause', async (t) => { t = tspl(t, { plan: 1 }) diff --git a/test/dispatcher.js b/test/dispatcher.js index d004c5e5e27..95a9bc59a83 100644 --- a/test/dispatcher.js +++ b/test/dispatcher.js @@ -3,7 +3,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test } = require('node:test') -const Dispatcher = require('../lib/dispatcher') +const Dispatcher = require('../lib/dispatcher/dispatcher') class PoorImplementation extends Dispatcher {} diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js index 93792871cb1..038c23b5af9 100644 --- a/test/fetch/fetch-timeouts.js +++ b/test/fetch/fetch-timeouts.js @@ -4,7 +4,7 @@ const { test } = require('node:test') const { tspl } = require('@matteo.collina/tspl') const { fetch, Agent } = require('../..') -const timers = require('../../lib/timers') +const timers = require('../../lib/util/timers') const { createServer } = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') const { closeServerAsPromise } = require('../utils/node-http') diff --git a/test/mock-agent.js b/test/mock-agent.js index 58066229f58..ab852896daf 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -11,7 +11,7 @@ const { InvalidArgumentError, ClientDestroyedError } = require('../lib/core/erro const MockClient = require('../lib/mock/mock-client') const MockPool = require('../lib/mock/mock-pool') const { kAgent } = require('../lib/mock/mock-symbols') -const Dispatcher = require('../lib/dispatcher') +const Dispatcher = require('../lib/dispatcher/dispatcher') const { MockNotMatchedError } = require('../lib/mock/mock-errors') const { fetch } = require('..') diff --git a/test/mock-client.js b/test/mock-client.js index ef6721b42be..9b8f416ec34 100644 --- a/test/mock-client.js +++ b/test/mock-client.js @@ -10,7 +10,7 @@ const { kDispatches } = require('../lib/mock/mock-symbols') const { InvalidArgumentError } = require('../lib/core/errors') const { MockInterceptor } = require('../lib/mock/mock-interceptor') const { getResponse } = require('../lib/mock/mock-utils') -const Dispatcher = require('../lib/dispatcher') +const Dispatcher = require('../lib/dispatcher/dispatcher') describe('MockClient - constructor', () => { test('fails if opts.agent does not implement `get` method', t => { diff --git a/test/mock-pool.js b/test/mock-pool.js index 0b690fe6d48..b52d3406cce 100644 --- a/test/mock-pool.js +++ b/test/mock-pool.js @@ -10,7 +10,7 @@ const { kDispatches } = require('../lib/mock/mock-symbols') const { InvalidArgumentError } = require('../lib/core/errors') const { MockInterceptor } = require('../lib/mock/mock-interceptor') const { getResponse } = require('../lib/mock/mock-utils') -const Dispatcher = require('../lib/dispatcher') +const Dispatcher = require('../lib/dispatcher/dispatcher') const { fetch } = require('..') describe('MockPool - constructor', () => { diff --git a/test/pool.js b/test/pool.js index 4a22aedc84e..5e84f4402bb 100644 --- a/test/pool.js +++ b/test/pool.js @@ -366,7 +366,7 @@ test('backpressure algorithm', async (t) => { } } - const Pool = proxyquire('../lib/pool', { + const Pool = proxyquire('../lib/dispatcher/pool', { './client': FakeClient }) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 8c6634ff1fb..0e33f9600ee 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -6,8 +6,8 @@ const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('.. const { InvalidArgumentError } = require('../lib/core/errors') const { readFileSync } = require('node:fs') const { join } = require('node:path') -const ProxyAgent = require('../lib/proxy-agent') -const Pool = require('../lib/pool') +const ProxyAgent = require('../lib/dispatcher/proxy-agent') +const Pool = require('../lib/dispatcher/pool') const { createServer } = require('node:http') const https = require('node:https') const proxy = require('proxy') diff --git a/test/request-timeout.js b/test/request-timeout.js index f49f6c5bcf3..71bff4fb9cc 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -5,7 +5,7 @@ const { test, after } = require('node:test') const { createReadStream, writeFileSync, unlinkSync } = require('node:fs') const { Client, errors } = require('..') const { kConnect } = require('../lib/core/symbols') -const timers = require('../lib/timers') +const timers = require('../lib/util/timers') const { createServer } = require('node:http') const EventEmitter = require('node:events') const FakeTimers = require('@sinonjs/fake-timers') diff --git a/test/socket-timeout.js b/test/socket-timeout.js index 83617e94f7e..5be62fe4119 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -3,7 +3,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client, errors } = require('..') -const timers = require('../lib/timers') +const timers = require('../lib/util/timers') const { createServer } = require('node:http') const FakeTimers = require('@sinonjs/fake-timers') From 7623f106e2ff9eb2cd9c38a80e0e7be0a78f0f61 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 23 Feb 2024 11:56:13 +0100 Subject: [PATCH 069/123] refactor: move fixed-queeu to dispatcher and rm node folder (#2827) --- lib/{node => dispatcher}/fixed-queue.js | 0 lib/dispatcher/pool-base.js | 2 +- test/fixed-queue.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/{node => dispatcher}/fixed-queue.js (100%) diff --git a/lib/node/fixed-queue.js b/lib/dispatcher/fixed-queue.js similarity index 100% rename from lib/node/fixed-queue.js rename to lib/dispatcher/fixed-queue.js diff --git a/lib/dispatcher/pool-base.js b/lib/dispatcher/pool-base.js index 6c524e2a437..93422f832e9 100644 --- a/lib/dispatcher/pool-base.js +++ b/lib/dispatcher/pool-base.js @@ -1,7 +1,7 @@ 'use strict' const DispatcherBase = require('./dispatcher-base') -const FixedQueue = require('../node/fixed-queue') +const FixedQueue = require('./fixed-queue') const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols') const PoolStats = require('./pool-stats') diff --git a/test/fixed-queue.js b/test/fixed-queue.js index cb878716e44..3cb1b392ff4 100644 --- a/test/fixed-queue.js +++ b/test/fixed-queue.js @@ -3,7 +3,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test } = require('node:test') -const FixedQueue = require('../lib/node/fixed-queue') +const FixedQueue = require('../lib/dispatcher/fixed-queue') test('fixed queue 1', (t) => { t = tspl(t, { plan: 5 }) From ef471e6120f88c9d8124bfcb565b1661002e1cbe Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 23 Feb 2024 13:16:16 +0100 Subject: [PATCH 070/123] chore: create package.json in benchmarks (#2766) * chore: create package.json in benchmarks * update deps of benchmarks --- .github/dependabot.yml | 6 ++++++ .github/workflows/bench.yml | 12 ++++++++++-- benchmarks/package.json | 20 ++++++++++++++++++++ package.json | 15 +-------------- 4 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 benchmarks/package.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 49b8cc8a17a..befcde3c066 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,12 @@ updates: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "npm" + directory: /benchmarks + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + - package-ecosystem: docker directory: /build schedule: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 545f5d9d081..5afcbddd807 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -20,10 +20,14 @@ jobs: uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* - - name: Install Modules + - name: Install Modules for undici + run: npm i --ignore-scripts --omit=dev + - name: Install Modules for Benchmarks run: npm i + working-directory: ./benchmarks - name: Run Benchmark run: npm run bench + working-directory: ./benchmarks benchmark_branch: name: benchmark branch @@ -37,7 +41,11 @@ jobs: uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: lts/* - - name: Install Modules + - name: Install Modules for undici + run: npm i --ignore-scripts --omit=dev + - name: Install Modules for Benchmarks run: npm i + working-directory: ./benchmarks - name: Run Benchmark run: npm run bench + working-directory: ./benchmarks diff --git a/benchmarks/package.json b/benchmarks/package.json new file mode 100644 index 00000000000..5781ae37478 --- /dev/null +++ b/benchmarks/package.json @@ -0,0 +1,20 @@ +{ + "name": "benchmarks", + "scripts": { + "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", + "bench:server": "node ./server.js", + "prebench:run": "node ./wait.js", + "bench:run": "SAMPLES=100 CONNECTIONS=50 node ./benchmark.js" + }, + "dependencies": { + "axios": "^1.6.7", + "concurrently": "^8.2.2", + "cronometro": "^3.0.1", + "got": "^14.2.0", + "mitata": "^0.1.11", + "node-fetch": "^3.3.2", + "request": "^2.88.2", + "superagent": "^8.1.2", + "wait-on": "^7.2.0" + } +} diff --git a/package.json b/package.json index 748b8098ca0..94e30d2736a 100644 --- a/package.json +++ b/package.json @@ -82,10 +82,7 @@ "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", "coverage": "nyc --reporter=text --reporter=html npm run test", "coverage:ci": "nyc --reporter=lcov npm run test", - "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", - "bench:server": "node benchmarks/server.js", - "prebench:run": "node benchmarks/wait.js", - "bench:run": "SAMPLES=100 CONNECTIONS=50 node benchmarks/benchmark.js", + "bench": "echo \"Error: Benchmarks have been moved to '\/benchmarks'\" && exit 1", "serve:website": "echo \"Error: Documentation has been moved to '\/docs'\" && exit 1", "prepare": "husky install", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" @@ -94,34 +91,24 @@ "@matteo.collina/tspl": "^0.1.1", "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", - "@types/superagent": "^8.1.3", "abort-controller": "^3.0.0", - "axios": "^1.6.5", "borp": "^0.9.1", - "concurrently": "^8.0.1", - "cronometro": "^3.0.1", "dns-packet": "^5.4.0", "form-data": "^4.0.0", "formdata-node": "^6.0.3", - "got": "^14.0.0", "https-pem": "^3.0.0", "husky": "^9.0.7", "import-fresh": "^3.3.0", "jest": "^29.0.2", "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", - "mitata": "^0.1.10", - "node-fetch": "^3.3.2", "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.1.3", - "request": "^2.88.2", "snazzy": "^9.0.0", "standard": "^17.0.0", - "superagent": "^8.1.2", "tsd": "^0.30.1", "typescript": "^5.0.2", - "wait-on": "^7.0.1", "ws": "^8.11.0" }, "engines": { From 3e3c3fc4769c41eefdddb8a05769114322e299a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:25:08 +0000 Subject: [PATCH 071/123] build(deps): bump github/codeql-action from 3.24.4 to 3.24.5 (#2829) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.4 to 3.24.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e2e140ad1441662206e8f97754b166877dfa1c73...47b3d888fe66b639e431abf22ebca059152f1eea) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 505bae5f8ce..6c2848d72e5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 + uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 + uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e2e140ad1441662206e8f97754b166877dfa1c73 # v2.3.3 + uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 623a220c74a..bbb98bf925e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e2e140ad1441662206e8f97754b166877dfa1c73 # v3.24.4 + uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 with: sarif_file: results.sarif From 669a9241238fb7599c85ccaad524db511f4709f5 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Fri, 23 Feb 2024 18:16:14 +0100 Subject: [PATCH 072/123] chore: use lts for pubish types workflow (#2830) --- .github/workflows/publish-undici-types.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index 2d56f755231..92bb1943138 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: - node-version: '16.x' + node-version: lts/* registry-url: 'https://registry.npmjs.org' - run: npm install - run: node scripts/generate-undici-types-package-json.js From df1e0c1d267ecdfdc9c4a94139dc98b30375a11a Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 23 Feb 2024 17:42:37 -0500 Subject: [PATCH 073/123] add dispatcher option to Request (#2831) --- lib/web/cache/cache.js | 2 -- lib/web/eventsource/eventsource.js | 6 +----- lib/web/fetch/index.js | 6 +++--- lib/web/fetch/request.js | 10 +++++++++- lib/web/fetch/symbols.js | 3 ++- lib/web/fetch/webidl.js | 2 +- lib/web/websocket/connection.js | 3 +-- test/fetch/issue-2828.js | 32 ++++++++++++++++++++++++++++++ 8 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 test/fetch/issue-2828.js diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js index acbd6c7d0f7..80d9e2f15f3 100644 --- a/lib/web/cache/cache.js +++ b/lib/web/cache/cache.js @@ -10,7 +10,6 @@ const { kState } = require('../fetch/symbols') const { fetching } = require('../fetch/index') const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') const assert = require('node:assert') -const { getGlobalDispatcher } = require('../../global') /** * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation @@ -150,7 +149,6 @@ class Cache { // 5.7 fetchControllers.push(fetching({ request: r, - dispatcher: getGlobalDispatcher(), processResponse (response) { // 1. if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index ad6ea26dcd1..6b34976e31b 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -9,7 +9,6 @@ const { EventSourceStream } = require('./eventsource-stream') const { parseMIMEType } = require('../fetch/dataURL') const { MessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') -const { getGlobalDispatcher } = require('../../global') const { delay } = require('./util') let experimentalWarned = false @@ -316,10 +315,7 @@ class EventSource extends EventTarget { }) } - this.#controller = fetching({ - ...fetchParam, - dispatcher: getGlobalDispatcher() - }) + this.#controller = fetching(fetchParam) } /** diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index df2f442c647..e9733362e95 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -47,7 +47,7 @@ const { createInflate, extractMimeType } = require('./util') -const { kState } = require('./symbols') +const { kState, kDispatcher } = require('./symbols') const assert = require('node:assert') const { safelyExtractBody, extractBody } = require('./body') const { @@ -239,7 +239,7 @@ function fetch (input, init = undefined) { request, processResponseEndOfBody: handleFetchDone, processResponse, - dispatcher: init?.dispatcher ?? getGlobalDispatcher() // undici + dispatcher: requestObject[kDispatcher] // undici }) // 14. Return p. @@ -361,7 +361,7 @@ function fetching ({ processResponseEndOfBody, processResponseConsumeBody, useParallelQueue = false, - dispatcher // undici + dispatcher = getGlobalDispatcher() // undici }) { // Ensure that the dispatcher is set accordingly assert(dispatcher) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index ef2f3ff0b14..f8759626a1e 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -24,7 +24,7 @@ const { requestDuplex } = require('./constants') const { kEnumerableProperty } = util -const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') +const { kHeaders, kSignal, kState, kGuard, kRealm, kDispatcher } = require('./symbols') const { webidl } = require('./webidl') const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') @@ -78,6 +78,8 @@ class Request { // 5. If input is a string, then: if (typeof input === 'string') { + this[kDispatcher] = init.dispatcher + // 1. Let parsedURL be the result of parsing input with baseURL. // 2. If parsedURL is failure, then throw a TypeError. let parsedURL @@ -101,6 +103,8 @@ class Request { // 5. Set fallbackMode to "cors". fallbackMode = 'cors' } else { + this[kDispatcher] = input[kDispatcher] + // 6. Otherwise: // 7. Assert: input is a Request object. @@ -979,6 +983,10 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ key: 'duplex', converter: webidl.converters.DOMString, allowedValues: requestDuplex + }, + { + key: 'dispatcher', // undici specific option + converter: webidl.converters.any } ]) diff --git a/lib/web/fetch/symbols.js b/lib/web/fetch/symbols.js index 0b947d55bad..e98e78080b6 100644 --- a/lib/web/fetch/symbols.js +++ b/lib/web/fetch/symbols.js @@ -6,5 +6,6 @@ module.exports = { kSignal: Symbol('signal'), kState: Symbol('state'), kGuard: Symbol('guard'), - kRealm: Symbol('realm') + kRealm: Symbol('realm'), + kDispatcher: Symbol('dispatcher') } diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index da5df4a362f..41f5813db69 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -3,7 +3,7 @@ const { types } = require('node:util') const { toUSVString } = require('../../core/util') -/** @type {import('../../types/webidl').Webidl} */ +/** @type {import('../../../types/webidl').Webidl} */ const webidl = {} webidl.converters = {} webidl.util = {} diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 33905404833..8f12933c959 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -13,7 +13,6 @@ const { CloseEvent } = require('./events') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') const { Headers } = require('../fetch/headers') -const { getGlobalDispatcher } = require('../../global') const { kHeadersList } = require('../../core/symbols') /** @type {import('crypto')} */ @@ -100,7 +99,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) const controller = fetching({ request, useParallelQueue: true, - dispatcher: options.dispatcher ?? getGlobalDispatcher(), + dispatcher: options.dispatcher, processResponse (response) { // 1. If response is a network error or its status is not 101, // fail the WebSocket connection. diff --git a/test/fetch/issue-2828.js b/test/fetch/issue-2828.js new file mode 100644 index 00000000000..08e8ea714ca --- /dev/null +++ b/test/fetch/issue-2828.js @@ -0,0 +1,32 @@ +'use strict' + +const { once } = require('node:events') +const { createServer } = require('node:http') +const { test } = require('node:test') +const { Agent, Request, fetch } = require('../..') +const { tspl } = require('@matteo.collina/tspl') + +test('issue #2828, dispatcher is allowed in RequestInit options', async (t) => { + const { deepStrictEqual } = tspl(t, { plan: 1 }) + + class CustomAgent extends Agent { + dispatch (options, handler) { + options.headers['x-my-header'] = 'hello' + return super.dispatch(...arguments) + } + } + + const server = createServer((req, res) => { + deepStrictEqual(req.headers['x-my-header'], 'hello') + res.end() + }).listen(0) + + t.after(server.close.bind(server)) + await once(server, 'listening') + + const request = new Request(`http://localhost:${server.address().port}`, { + dispatcher: new CustomAgent() + }) + + await fetch(request) +}) From 3a9723dc6eec2824a751c6ade2cd29760be1f64a Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 24 Feb 2024 04:31:06 -0500 Subject: [PATCH 074/123] fix url referrer wpt (#2832) --- lib/web/fetch/util.js | 2 ++ test/wpt/status/fetch.status.json | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index b3523cb2e15..37c8c78fc5d 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -437,6 +437,8 @@ function stripURLForReferrer (url, originOnly) { // 1. Assert: url is a URL. assert(url instanceof URL) + url = new URL(url) + // 2. If url’s scheme is a local scheme, then return no referrer. if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { return 'no-referrer' diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 860e053b3c5..3b2bfa002f0 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -76,10 +76,8 @@ "skip": true }, "request-referrer.any.js": { - "note": "TODO(@KhafraDev): url referrer test could probably be fixed", "fail": [ - "about:client referrer", - "url referrer" + "about:client referrer" ] }, "request-upload.any.js": { From cc7ee587557bfd186f8ddae86bc2391a61657351 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:31:43 +0900 Subject: [PATCH 075/123] refactor: remove sort logic (#2834) --- benchmarks/sort.mjs | 50 ----------- lib/web/fetch/headers.js | 3 +- lib/web/fetch/sort.js | 187 --------------------------------------- test/fetch/sort.js | 90 ------------------- 4 files changed, 1 insertion(+), 329 deletions(-) delete mode 100644 benchmarks/sort.mjs delete mode 100644 lib/web/fetch/sort.js delete mode 100644 test/fetch/sort.js diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs deleted file mode 100644 index bc5d2bc71ed..00000000000 --- a/benchmarks/sort.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { bench, group, run } from 'mitata' -import { sort, heapSort, introSort } from '../lib/web/fetch/sort.js' - -function compare (a, b) { - return a < b ? -1 : 1 -} - -const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' -const charactersLength = characters.length - -function generateAsciiString (length) { - let result = '' - for (let i = 0; i < length; ++i) { - result += characters[Math.floor(Math.random() * charactersLength)] - } - return result -} - -const settings = { - tiny: 32, - small: 64, - middle: 128, - large: 512 -} - -for (const [name, length] of Object.entries(settings)) { - group(`sort (${name})`, () => { - const array = Array.from(new Array(length), () => generateAsciiString(12)) - // sort(array, compare) - bench('Array#sort', () => array.slice().sort(compare)) - bench('sort (intro sort)', () => sort(array.slice(), compare)) - - // sort(array, start, end, compare) - bench('intro sort', () => introSort(array.slice(), 0, array.length, compare)) - bench('heap sort', () => heapSort(array.slice(), 0, array.length, compare)) - }) - - group(`sort sortedArray (${name})`, () => { - const array = Array.from(new Array(length), () => generateAsciiString(12)).sort(compare) - // sort(array, compare) - bench('Array#sort', () => array.sort(compare)) - bench('sort (intro sort)', () => sort(array, compare)) - - // sort(array, start, end, compare) - bench('intro sort', () => introSort(array, 0, array.length, compare)) - bench('heap sort', () => heapSort(array, 0, array.length, compare)) - }) -} - -await run() diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index bdb53b52654..f21c33b438d 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -12,7 +12,6 @@ const { } = require('./util') const { webidl } = require('./webidl') const assert = require('node:assert') -const { sort } = require('./sort') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') @@ -328,7 +327,7 @@ class HeadersList { // 3.2.2. Assert: value is non-null. assert(value !== null) } - return sort(array, compareHeaderName) + return array.sort(compareHeaderName) } } } diff --git a/lib/web/fetch/sort.js b/lib/web/fetch/sort.js deleted file mode 100644 index 230f2e2645c..00000000000 --- a/lib/web/fetch/sort.js +++ /dev/null @@ -1,187 +0,0 @@ -'use strict' - -/** **binary insertion sort** - * - Best -> O(n) - * - Average -> O(n^2) - * - Worst -> O(n^2) - * - Memory -> O(n) total, O(1) auxiliary - * - Stable -> true - * @param {any[]} array - * @param {number} begin begin - * @param {number} end end - * @param {(a: any, b: any) => number} compare - */ -function binaryInsertionSort (array, begin, end, compare) { - for ( - let i = begin + 1, j = 0, right = 0, left = 0, pivot = 0, x; - i < end; - ++i - ) { - x = array[i] - left = 0 - right = i - // binary search - while (left < right) { - // middle index - pivot = left + ((right - left) >> 1) - if (compare(array[pivot], x) <= 0) { - left = pivot + 1 - } else { - right = pivot - } - } - if (i !== pivot) { - j = i - while (j > left) { - array[j] = array[--j] - } - array[left] = x - } - } - return array -} - -/** - * @param {number} num - */ -function log2 (num) { - // Math.floor(Math.log2(num)) - let log = 0 - // eslint-disable-next-line no-cond-assign - while ((num >>= 1)) ++log - return log -} - -/** **intro sort** - * - Average -> O(n log n) - * - Worst -> O(n log n) - * - Stable -> false - * @param {any[]} array - * @param {number} begin begin - * @param {number} end end - * @param {(a: any, b: any) => number} compare - */ -function introSort (array, begin, end, compare) { - return _introSort(array, begin, end, log2(end - begin) << 1, compare) -} - -/** - * @param {any[]} array - * @param {number} begin - * @param {number} end - * @param {number} depth - * @param {(a: any, b: any) => number} compare - */ -function _introSort (array, begin, end, depth, compare) { - if (end - begin <= 32) { - return binaryInsertionSort(array, begin, end, compare) - } - if (depth-- <= 0) { - return heapSort(array, begin, end, compare) - } - // median of three quick sort - let i = begin - let j = end - 1 - const pivot = medianOf3( - array[i], - array[i + ((j - i) >> 1)], - array[j], - compare - ) - let firstPass = true - while (true) { - while (compare(array[i], pivot) < 0) ++i - while (compare(pivot, array[j]) < 0) --j - if (i >= j) break; - [array[i], array[j]] = [array[j], array[i]] - ++i - --j - firstPass = false - } - if (i - begin > 1 && !firstPass) _introSort(array, begin, i, depth, compare) - // if (end - (j + 1) > 1) ... - if (end - j > 2 && !firstPass) _introSort(array, j + 1, end, depth, compare) - return array -} - -/** **heap sort (bottom up)** - * - Best -> Ω(n) - * - Average -> O(n log n) - * - Worst -> O(n log n) - * - Memory -> O(n) total, O(1) auxiliary - * - Stable -> false - * @param {any[]} array - * @param {number} begin - * @param {number} end - * @param {(a: any, b: any) => number} compare - */ -function heapSort (array, begin, end, compare) { - const N = end - begin - let p = N >> 1 - let q = N - 1 - let x - while (p > 0) { - downHeap(array, array[begin + p - 1], begin, --p, q, compare) - } - while (q > 0) { - x = array[begin + q] - array[begin + q] = array[begin] - downHeap(array, (array[begin] = x), begin, 0, --q, compare) - } - return array -} - -/** - * @param {any[]} array - * @param {any} x - * @param {number} begin - * @param {number} p - * @param {number} q - * @param {(a: any, b: any) => number} compare - */ -function downHeap (array, x, begin, p, q, compare) { - let c - while ((c = (p << 1) + 1) <= q) { - if (c < q && compare(array[begin + c], array[begin + c + 1]) < 0) ++c - if (compare(x, array[begin + c]) >= 0) break - array[begin + p] = array[begin + c] - p = c - } - array[begin + p] = x -} - -/** - * @param {any} x - * @param {any} y - * @param {any} z - * @param {(a: any, b: any) => number} compare - */ -function medianOf3 (x, y, z, compare) { - return compare(x, y) < 0 - ? compare(y, z) < 0 - ? y - : compare(z, x) < 0 - ? x - : z - : compare(z, y) < 0 - ? y - : compare(x, z) < 0 - ? x - : z -} - -/** - * @param {any[]} array - * @param {(a: any, b: any) => number} compare - */ -function sort (array, compare) { - const length = array.length - return _introSort(array, 0, length, log2(length) << 1, compare) -} - -module.exports = { - sort, - binaryInsertionSort, - introSort, - heapSort -} diff --git a/test/fetch/sort.js b/test/fetch/sort.js deleted file mode 100644 index b2eef9ca699..00000000000 --- a/test/fetch/sort.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -const { describe, test } = require('node:test') -const assert = require('node:assert') -const { sort, heapSort, binaryInsertionSort, introSort } = require('../../lib/web/fetch/sort') - -function generateRandomNumberArray (length) { - const array = new Uint16Array(length) - for (let i = 0; i < length; ++i) { - array[i] = (65535 * Math.random()) | 0 - } - return array -} - -const compare = (a, b) => a - b - -const SORT_RUN = 4000 - -const SORT_ELEMENT = 200 - -describe('sort', () => { - const arrays = new Array(SORT_RUN) - const expectedArrays = new Array(SORT_RUN) - - for (let i = 0; i < SORT_RUN; ++i) { - const array = generateRandomNumberArray(SORT_ELEMENT) - const expected = array.slice().sort(compare) - arrays[i] = array - expectedArrays[i] = expected - } - - test('binary insertion sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('heap sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('intro sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) - } - }) -}) - -describe('sorted', () => { - const arrays = new Array(SORT_RUN) - const expectedArrays = new Array(SORT_RUN) - - for (let i = 0; i < SORT_RUN; ++i) { - const array = generateRandomNumberArray(SORT_ELEMENT).sort(compare) - arrays[i] = array - expectedArrays[i] = array.slice() - } - - test('binary insertion sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('heap sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('intro sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) - } - }) - - test('sort', () => { - for (let i = 0; i < SORT_RUN; ++i) { - assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) - } - }) -}) From 2a368b2bb31ce7d05d593bcfee1b212ab7218906 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Sat, 24 Feb 2024 08:54:58 -0800 Subject: [PATCH 076/123] fix(fetch): prevent crash when `fetch` is aborted with `null` as the `AbortSignal's` `reason` (#2833) --- index-fetch.js | 2 +- index.js | 2 +- test/fetch/issue-2242.js | 60 ++++++++++++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/index-fetch.js b/index-fetch.js index b8b3f3c7cac..dc6c0d05e3e 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -4,7 +4,7 @@ const fetchImpl = require('./lib/web/fetch').fetch module.exports.fetch = function fetch (resource, init = undefined) { return fetchImpl(resource, init).catch((err) => { - if (typeof err === 'object') { + if (err && typeof err === 'object') { Error.captureStackTrace(err, this) } throw err diff --git a/index.js b/index.js index 4c7a979f001..77fa2b9ffde 100644 --- a/index.js +++ b/index.js @@ -101,7 +101,7 @@ module.exports.fetch = async function fetch (init, options = undefined) { try { return await fetchImpl(init, options) } catch (err) { - if (typeof err === 'object') { + if (err && typeof err === 'object') { Error.captureStackTrace(err, this) } diff --git a/test/fetch/issue-2242.js b/test/fetch/issue-2242.js index ca12cc61c09..1c0fe1f4303 100644 --- a/test/fetch/issue-2242.js +++ b/test/fetch/issue-2242.js @@ -1,24 +1,54 @@ 'use strict' -const { test } = require('node:test') +const { beforeEach, describe, it } = require('node:test') const assert = require('node:assert') const { fetch } = require('../..') const nodeFetch = require('../../index-fetch') -test('fetch with signal already aborted', async () => { - await assert.rejects( - fetch('http://localhost', { - signal: AbortSignal.abort('Already aborted') - }), - /Already aborted/ +describe('Issue #2242', () => { + ['Already aborted', null, false, true, 123, Symbol('Some reason')].forEach( + (reason) => + describe(`when an already-aborted signal's reason is \`${String( + reason + )}\``, () => { + let signal + beforeEach(() => { + signal = AbortSignal.abort(reason) + }) + it('rejects with that reason ', async () => { + await assert.rejects(fetch('http://localhost', { signal }), (err) => { + assert.strictEqual(err, reason) + return true + }) + }) + it('rejects with that reason (from index-fetch)', async () => { + await assert.rejects( + nodeFetch.fetch('http://localhost', { signal }), + (err) => { + assert.strictEqual(err, reason) + return true + } + ) + }) + }) ) -}) -test('fetch with signal already aborted (from index-fetch)', async () => { - await assert.rejects( - nodeFetch.fetch('http://localhost', { - signal: AbortSignal.abort('Already aborted') - }), - /Already aborted/ - ) + describe("when an already-aborted signal's reason is `undefined`", () => { + let signal + beforeEach(() => { + signal = AbortSignal.abort(undefined) + }) + it('rejects with an `AbortError`', async () => { + await assert.rejects( + fetch('http://localhost', { signal }), + new DOMException('This operation was aborted', 'AbortError') + ) + }) + it('rejects with an `AbortError` (from index-fetch)', async () => { + await assert.rejects( + nodeFetch.fetch('http://localhost', { signal }), + new DOMException('This operation was aborted', 'AbortError') + ) + }) + }) }) From 95bd9290c4e5e523ee30d7a5a78df838515e6f63 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 25 Feb 2024 12:36:51 +0100 Subject: [PATCH 077/123] refactor: avoid http2 dynamic dispatch in socket handlers (#2839) --- lib/core/symbols.js | 3 +- lib/dispatcher/client.js | 238 ++++++++++++++++++++++----------------- 2 files changed, 137 insertions(+), 104 deletions(-) diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 68d8566fac0..d3edb65dda6 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -59,5 +59,6 @@ module.exports = { kHTTP2CopyHeaders: Symbol('http2 copy headers'), kHTTPConnVersion: Symbol('http connection version'), kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), - kConstruct: Symbol('constructable') + kConstruct: Symbol('constructable'), + kListeners: Symbol('listeners') } diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index cb3feabb37b..b4ca2f85428 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -72,6 +72,7 @@ const { kLocalAddress, kMaxResponseSize, kHTTPConnVersion, + kListeners, // HTTP2 kHost, kHTTP2Session, @@ -111,6 +112,20 @@ const FastBuffer = Buffer[Symbol.species] const kClosedResolve = Symbol('kClosedResolve') +function addListener (obj, name, listener) { + const listeners = (obj[kListeners] ??= []) + listeners.push([name, listener]) + obj.on(name, listener) + return obj +} + +function removeAllListeners (obj) { + for (const [name, listener] of obj[kListeners] ?? []) { + obj.removeListener(name, listener) + } + obj[kListeners] = null +} + /** * @type {import('../../types/client.js').default} */ @@ -276,7 +291,7 @@ class Client extends DispatcherBase { this[kMaxRequests] = maxRequestsPerClient this[kClosedResolve] = null this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 - this[kHTTPConnVersion] = 'h1' + this[kHTTPConnVersion] = null // HTTP/2 this[kHTTP2Session] = null @@ -803,11 +818,8 @@ class Parser { socket[kClient] = null socket[kError] = null - socket - .removeListener('error', onSocketError) - .removeListener('readable', onSocketReadable) - .removeListener('end', onSocketEnd) - .removeListener('close', onSocketClose) + + removeAllListeners(socket) client[kSocket] = null client[kHTTP2Session] = null @@ -1050,33 +1062,6 @@ function onParserTimeout (parser) { } } -function onSocketReadable () { - const { [kParser]: parser } = this - if (parser) { - parser.readMore() - } -} - -function onSocketError (err) { - const { [kClient]: client, [kParser]: parser } = this - - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - - if (client[kHTTPConnVersion] !== 'h2') { - // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded - // to the user. - if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() - return - } - } - - this[kError] = err - - onError(this[kClient], err) -} - function onError (client, err) { if ( client[kRunning] === 0 && @@ -1097,32 +1082,8 @@ function onError (client, err) { } } -function onSocketEnd () { - const { [kParser]: parser, [kClient]: client } = this - - if (client[kHTTPConnVersion] !== 'h2') { - if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() - return - } - } - - util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) -} - function onSocketClose () { - const { [kClient]: client, [kParser]: parser } = this - - if (client[kHTTPConnVersion] === 'h1' && parser) { - if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() - } - - this[kParser].destroy() - this[kParser] = null - } + const { [kClient]: client } = this const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) @@ -1215,56 +1176,19 @@ async function connect (client) { assert(socket) - const isH2 = socket.alpnProtocol === 'h2' - if (isH2) { - if (!h2ExperimentalWarned) { - h2ExperimentalWarned = true - process.emitWarning('H2 support is experimental, expect them to change at any time.', { - code: 'UNDICI-H2' - }) - } - - const session = http2.connect(client[kUrl], { - createConnection: () => socket, - peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams - }) - - client[kHTTPConnVersion] = 'h2' - session[kClient] = client - session[kSocket] = socket - session.on('error', onHttp2SessionError) - session.on('frameError', onHttp2FrameError) - session.on('end', onHttp2SessionEnd) - session.on('goaway', onHTTP2GoAway) - session.on('close', onSocketClose) - session.unref() - - client[kHTTP2Session] = session - socket[kHTTP2Session] = session + if (socket.alpnProtocol === 'h2') { + await connectH2(client, socket) } else { - if (!llhttpInstance) { - llhttpInstance = await llhttpPromise - llhttpPromise = null - } - - socket[kNoRef] = false - socket[kWriting] = false - socket[kReset] = false - socket[kBlocking] = false - socket[kParser] = new Parser(client, socket, llhttpInstance) + await connectH1(client, socket) } + addListener(socket, 'close', onSocketClose) + socket[kCounter] = 0 socket[kMaxRequests] = client[kMaxRequests] socket[kClient] = client socket[kError] = null - socket - .on('error', onSocketError) - .on('readable', onSocketReadable) - .on('end', onSocketEnd) - .on('close', onSocketClose) - client[kSocket] = socket if (channels.connected.hasSubscribers) { @@ -1475,10 +1399,15 @@ function shouldSendContentLength (method) { function write (client, request) { if (client[kHTTPConnVersion] === 'h2') { - writeH2(client, client[kHTTP2Session], request) - return + // TODO (fix): Why does this not return the value + // from writeH2. + writeH2(client, request) + } else { + return writeH1(client, request) } +} +function writeH1 (client, request) { const { method, path, host, upgrade, blocking, reset } = request let { body, headers, contentLength } = request @@ -1656,7 +1585,8 @@ function write (client, request) { return true } -function writeH2 (client, session, request) { +function writeH2 (client, request) { + const session = client[kHTTP2Session] const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request let headers @@ -2341,4 +2271,106 @@ function errorRequest (client, request, err) { } } +async function connectH1 (client, socket) { + client[kHTTPConnVersion] = 'h1' + + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + + addListener(socket, 'error', function (err) { + const { [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + + this[kError] = err + + onError(this[kClient], err) + }) + addListener(socket, 'readable', function () { + const { [kParser]: parser } = this + if (parser) { + parser.readMore() + } + }) + addListener(socket, 'end', function () { + const { [kParser]: parser } = this + + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) + }) + addListener(socket, 'close', function () { + const { [kParser]: parser } = this + + if (parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + }) +} + +async function connectH2 (client, socket) { + client[kHTTPConnVersion] = 'h2' + + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + + addListener(socket, 'error', function (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kError] = err + + onError(this[kClient], err) + }) + addListener(socket, 'end', function () { + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) + }) +} + module.exports = Client From a2baaf52b4f153a51ccd0da0ea68ff02dbbfe7a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 14:56:14 +0100 Subject: [PATCH 078/123] build(deps-dev): bump proxy from 1.0.2 to 2.1.1 (#2137) --- docs/docs/best-practices/proxy.md | 12 ++++----- package.json | 2 +- test/proxy-agent.js | 38 +++++++++++++++------------- test/proxy.js | 42 ++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 29 deletions(-) diff --git a/docs/docs/best-practices/proxy.md b/docs/docs/best-practices/proxy.md index bf102955cc8..60f3372cf75 100644 --- a/docs/docs/best-practices/proxy.md +++ b/docs/docs/best-practices/proxy.md @@ -17,7 +17,7 @@ If you proxy requires basic authentication, you can send it via the `proxy-autho ```js import { Client } from 'undici' import { createServer } from 'http' -import proxy from 'proxy' +import { createProxy } from 'proxy' const server = await buildServer() const proxyServer = await buildProxy() @@ -59,7 +59,7 @@ function buildServer () { function buildProxy () { return new Promise((resolve, reject) => { - const server = proxy(createServer()) + const server = createProxy(createServer()) server.listen(0, () => resolve(server)) }) } @@ -70,7 +70,7 @@ function buildProxy () { ```js import { Client } from 'undici' import { createServer } from 'http' -import proxy from 'proxy' +import { createProxy } from 'proxy' const server = await buildServer() const proxyServer = await buildProxy() @@ -78,8 +78,8 @@ const proxyServer = await buildProxy() const serverUrl = `http://localhost:${server.address().port}` const proxyUrl = `http://localhost:${proxyServer.address().port}` -proxyServer.authenticate = function (req, fn) { - fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) +proxyServer.authenticate = function (req) { + return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}` } server.on('request', (req, res) => { @@ -119,7 +119,7 @@ function buildServer () { function buildProxy () { return new Promise((resolve, reject) => { - const server = proxy(createServer()) + const server = createProxy(createServer()) server.listen(0, () => resolve(server)) }) } diff --git a/package.json b/package.json index 94e30d2736a..9cd9f7431cc 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", "pre-commit": "^1.2.2", - "proxy": "^1.0.2", + "proxy": "^2.1.1", "proxyquire": "^2.1.3", "snazzy": "^9.0.0", "standard": "^17.0.0", diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 0e33f9600ee..200f3066c6a 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -10,7 +10,7 @@ const ProxyAgent = require('../lib/dispatcher/proxy-agent') const Pool = require('../lib/dispatcher/pool') const { createServer } = require('node:http') const https = require('node:https') -const proxy = require('proxy') +const { createProxy } = require('proxy') test('should throw error when no uri is provided', (t) => { t = tspl(t, { plan: 2 }) @@ -81,16 +81,16 @@ test('use proxy agent to connect through proxy using Pool', async (t) => { let resolveFirstConnect let connectCount = 0 - proxy.authenticate = async function (req, fn) { + proxy.authenticate = async function (req) { if (++connectCount === 2) { t.ok(true, 'second connect should arrive while first is still inflight') resolveFirstConnect() - fn(null, true) + return true } else { await new Promise((resolve) => { resolveFirstConnect = resolve }) - fn(null, true) + return true } } @@ -161,7 +161,7 @@ test('use proxy-agent to connect through proxy with basic auth in URL', async (t proxy.authenticate = function (req, fn) { t.ok(true, 'authentication should be called') - fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) + return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}` } proxy.on('connect', () => { t.ok(true, 'proxy should be called') @@ -203,9 +203,9 @@ test('use proxy-agent with auth', async (t) => { }) const parsedOrigin = new URL(serverUrl) - proxy.authenticate = function (req, fn) { + proxy.authenticate = function (req) { t.ok(true, 'authentication should be called') - fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) + return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}` } proxy.on('connect', () => { t.ok(true, 'proxy should be called') @@ -247,9 +247,9 @@ test('use proxy-agent with token', async (t) => { }) const parsedOrigin = new URL(serverUrl) - proxy.authenticate = function (req, fn) { + proxy.authenticate = function (req) { t.ok(true, 'authentication should be called') - fn(null, req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`) + return req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}` } proxy.on('connect', () => { t.ok(true, 'proxy should be called') @@ -460,7 +460,7 @@ test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async }) test('should throw when proxy does not return 200', async (t) => { - t = tspl(t, { plan: 2 }) + t = tspl(t, { plan: 3 }) const server = await buildServer() const proxy = await buildProxy() @@ -468,8 +468,9 @@ test('should throw when proxy does not return 200', async (t) => { const serverUrl = `http://localhost:${server.address().port}` const proxyUrl = `http://localhost:${proxy.address().port}` - proxy.authenticate = function (req, fn) { - fn(null, false) + proxy.authenticate = function (_req) { + t.ok(true, 'should call authenticate') + return false } const proxyAgent = new ProxyAgent(proxyUrl) @@ -488,15 +489,16 @@ test('should throw when proxy does not return 200', async (t) => { }) test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => { - t = tspl(t, { plan: 1 }) + t = tspl(t, { plan: 2 }) const server = await buildServer() const proxy = await buildProxy() const serverUrl = `http://localhost:${server.address().port}` const proxyUrl = `http://localhost:${proxy.address().port}` - proxy.authenticate = function (req, fn) { - fn(null, false) + proxy.authenticate = function (_req) { + t.ok(true, 'should call authenticate') + return false } const proxyAgent = new ProxyAgent(proxyUrl) @@ -742,8 +744,8 @@ function buildSSLServer () { function buildProxy (listener) { return new Promise((resolve) => { const server = listener - ? proxy(createServer(listener)) - : proxy(createServer()) + ? createProxy(createServer(listener)) + : createProxy(createServer()) server.listen(0, () => resolve(server)) }) } @@ -758,7 +760,7 @@ function buildSSLProxy () { } return new Promise((resolve) => { - const server = proxy(https.createServer(serverOptions)) + const server = createProxy(https.createServer(serverOptions)) server.listen(0, () => resolve(server)) }) } diff --git a/test/proxy.js b/test/proxy.js index e57babc8bc5..30d1290a6ff 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test } = require('node:test') const { Client, Pool } = require('..') const { createServer } = require('node:http') -const proxy = require('proxy') +const { createProxy } = require('proxy') test('connect through proxy', async (t) => { t = tspl(t, { plan: 3 }) @@ -50,8 +50,8 @@ test('connect through proxy with auth', async (t) => { const serverUrl = `http://localhost:${server.address().port}` const proxyUrl = `http://localhost:${proxy.address().port}` - proxy.authenticate = function (req, fn) { - fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`) + proxy.authenticate = function (req) { + return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}` } server.on('request', (req, res) => { @@ -83,6 +83,40 @@ test('connect through proxy with auth', async (t) => { client.close() }) +test('connect through proxy with auth but invalid credentials', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + + proxy.authenticate = function (req) { + return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:no-pass').toString('base64')}` + } + + server.on('request', (req, res) => { + t.fail('should not be called') + }) + + const client = new Client(proxyUrl) + + const response = await client.request({ + method: 'GET', + path: serverUrl + '/hello?foo=bar', + headers: { + 'proxy-authorization': `Basic ${Buffer.from('user:pass').toString('base64')}` + } + }) + + t.strictEqual(response.statusCode, 407) + + server.close() + proxy.close() + client.close() +}) + test('connect through proxy (with pool)', async (t) => { t = tspl(t, { plan: 3 }) @@ -127,7 +161,7 @@ function buildServer () { function buildProxy () { return new Promise((resolve, reject) => { - const server = proxy(createServer()) + const server = createProxy(createServer()) server.listen(0, () => resolve(server)) }) } From 21832bff8ab4ad7b676a9fd8db1841b5ec9eb95a Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:00:01 +0900 Subject: [PATCH 079/123] perf(tree): reduce overhead of build TernarySearchTree (#2840) --- lib/core/tree.js | 20 ++++++++++++++------ test/node-test/tree.js | 26 +++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/core/tree.js b/lib/core/tree.js index 9b50767c6d3..17dfca4cc4e 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -17,7 +17,7 @@ class TstNode { /** @type {number} */ code /** - * @param {Uint8Array} key + * @param {string} key * @param {any} value * @param {number} index */ @@ -25,7 +25,11 @@ class TstNode { if (index === undefined || index >= key.length) { throw new TypeError('Unreachable') } - this.code = key[index] + const code = this.code = key.charCodeAt(index) + // check code is ascii string + if (code > 0x7F) { + throw new TypeError('key must be ascii string') + } if (key.length !== ++index) { this.middle = new TstNode(key, value, index) } else { @@ -34,7 +38,7 @@ class TstNode { } /** - * @param {Uint8Array} key + * @param {string} key * @param {any} value */ add (key, value) { @@ -45,7 +49,11 @@ class TstNode { let index = 0 let node = this while (true) { - const code = key[index] + const code = key.charCodeAt(index) + // check code is ascii string + if (code > 0x7F) { + throw new TypeError('key must be ascii string') + } if (node.code === code) { if (length === ++index) { node.value = value @@ -111,7 +119,7 @@ class TernarySearchTree { node = null /** - * @param {Uint8Array} key + * @param {string} key * @param {any} value * */ insert (key, value) { @@ -135,7 +143,7 @@ const tree = new TernarySearchTree() for (let i = 0; i < wellknownHeaderNames.length; ++i) { const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]] - tree.insert(Buffer.from(key), key) + tree.insert(key, key) } module.exports = { diff --git a/test/node-test/tree.js b/test/node-test/tree.js index 44a7d7960ac..778db4c7c39 100644 --- a/test/node-test/tree.js +++ b/test/node-test/tree.js @@ -7,26 +7,34 @@ const assert = require('node:assert') describe('Ternary Search Tree', () => { test('The empty key cannot be added.', () => { - assert.throws(() => new TernarySearchTree().insert(Buffer.from(''), '')) + assert.throws(() => new TernarySearchTree().insert('', '')) const tst = new TernarySearchTree() - tst.insert(Buffer.from('a'), 'a') - assert.throws(() => tst.insert(Buffer.from(''), '')) + tst.insert('a', 'a') + assert.throws(() => tst.insert('', '')) }) test('looking up not inserted key returns null', () => { - assert.throws(() => new TernarySearchTree().insert(Buffer.from(''), '')) const tst = new TernarySearchTree() - tst.insert(Buffer.from('a'), 'a') + tst.insert('a', 'a') assert.strictEqual(tst.lookup(Buffer.from('non-existant')), null) }) + test('not ascii string', () => { + assert.throws(() => new TernarySearchTree().insert('\x80', 'a')) + const tst = new TernarySearchTree() + tst.insert('a', 'a') + // throw on TstNode + assert.throws(() => tst.insert('\x80', 'a')) + }) + test('duplicate key', () => { const tst = new TernarySearchTree() - const key = Buffer.from('a') + const key = 'a' + const lookupKey = Buffer.from(key) tst.insert(key, 'a') - assert.strictEqual(tst.lookup(key), 'a') + assert.strictEqual(tst.lookup(lookupKey), 'a') tst.insert(key, 'b') - assert.strictEqual(tst.lookup(key), 'b') + assert.strictEqual(tst.lookup(lookupKey), 'b') }) test('tree', () => { @@ -59,7 +67,7 @@ describe('Ternary Search Tree', () => { const key = generateAsciiString((Math.random() * 100 + 5) | 0) const lowerCasedKey = random[i] = key.toLowerCase() randomBuffer[i] = Buffer.from(key) - tst.insert(Buffer.from(lowerCasedKey), lowerCasedKey) + tst.insert(lowerCasedKey, lowerCasedKey) } for (let i = 0; i < LENGTH; ++i) { From c49058b956bda999bc26a1c43c66f1bede2ebc18 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 25 Feb 2024 11:05:39 -0500 Subject: [PATCH 080/123] webidl: implement resizable arraybuffer checks (#2094) --- lib/web/fetch/webidl.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index 41f5813db69..f111162a10d 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -536,7 +536,12 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V) is true, then throw a // TypeError. - // Note: resizable ArrayBuffers are currently a proposal. + if (V.resizable || V.growable) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'Received a resizable ArrayBuffer.' + }) + } // 4. Return the IDL ArrayBuffer value that is a // reference to the same object as V. @@ -576,7 +581,12 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - // Note: resizable array buffers are currently a proposal + if (V.buffer.resizable || V.buffer.growable) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'Received a resizable ArrayBuffer.' + }) + } // 5. Return the IDL value of type T that is a reference // to the same object as V. @@ -608,7 +618,12 @@ webidl.converters.DataView = function (V, opts = {}) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - // Note: resizable ArrayBuffers are currently a proposal + if (V.buffer.resizable || V.buffer.growable) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'Received a resizable ArrayBuffer.' + }) + } // 4. Return the IDL DataView value that is a reference // to the same object as V. From 0abea110907b229dff49caa5902541fe74fce4ff Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 25 Feb 2024 19:33:52 -0500 Subject: [PATCH 081/123] websocket server only needs to reply with a single subprotocol (#2845) * websocket server only needs to reply with a single subprotocol * fixup * fixup * fixup --- lib/web/fetch/util.js | 3 +- lib/web/websocket/connection.js | 16 ++++++-- test/websocket/issue-2844.js | 73 +++++++++++++++++++++++++++++++++ test/wpt/server/websocket.mjs | 2 +- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 test/websocket/issue-2844.js diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index 37c8c78fc5d..49ab59e8876 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -1476,5 +1476,6 @@ module.exports = { buildContentRange, parseMetadata, createInflate, - extractMimeType + extractMimeType, + getDecodeSplit } diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 8f12933c959..45f68e1de93 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -13,6 +13,7 @@ const { CloseEvent } = require('./events') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') const { Headers } = require('../fetch/headers') +const { getDecodeSplit } = require('../fetch/util') const { kHeadersList } = require('../../core/symbols') /** @type {import('crypto')} */ @@ -176,9 +177,18 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) // the WebSocket Connection_. const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') - if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { - failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') - return + if (secProtocol !== null) { + const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList) + + // The client can request that the server use a specific subprotocol by + // including the |Sec-WebSocket-Protocol| field in its handshake. If it + // is specified, the server needs to include the same field and one of + // the selected subprotocol values in its response for the connection to + // be established. + if (!requestProtocols.includes(secProtocol)) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } } response.socket.on('data', onSocketData) diff --git a/test/websocket/issue-2844.js b/test/websocket/issue-2844.js new file mode 100644 index 00000000000..d103a1722ff --- /dev/null +++ b/test/websocket/issue-2844.js @@ -0,0 +1,73 @@ +'use strict' + +const { test } = require('node:test') +const { once } = require('node:events') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') +const { tspl } = require('@matteo.collina/tspl') + +test('The server must reply with at least one subprotocol the client sends', async (t) => { + const { completed, deepStrictEqual, fail } = tspl(t, { plan: 2 }) + + const wss = new WebSocketServer({ + handleProtocols: (protocols) => { + deepStrictEqual(protocols, new Set(['msgpack', 'json'])) + + return protocols.values().next().value + }, + port: 0 + }) + + wss.on('connection', (ws) => { + ws.on('error', fail) + ws.send('something') + }) + + await once(wss, 'listening') + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + protocols: ['msgpack', 'json'] + }) + + ws.onerror = fail + ws.onopen = () => deepStrictEqual(ws.protocol, 'msgpack') + + t.after(() => { + wss.close() + ws.close() + }) + + await completed +}) + +test('The connection fails when the client sends subprotocols that the server does not responc with', async (t) => { + const { completed, fail, ok } = tspl(t, { plan: 1 }) + + const wss = new WebSocketServer({ + handleProtocols: () => false, + port: 0 + }) + + wss.on('connection', (ws) => { + ws.on('error', fail) + ws.send('something') + }) + + await once(wss, 'listening') + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + protocols: ['json'] + }) + + ws.onerror = ok.bind(null, true) + // The server will try to send 'something', this ensures that the connection + // fails during the handshake and doesn't receive any messages. + ws.onmessage = fail + + t.after(() => { + wss.close() + ws.close() + }) + + await completed +}) diff --git a/test/wpt/server/websocket.mjs b/test/wpt/server/websocket.mjs index cc8ce78151b..9bb05d12612 100644 --- a/test/wpt/server/websocket.mjs +++ b/test/wpt/server/websocket.mjs @@ -8,7 +8,7 @@ import { server } from './server.mjs' const wss = new WebSocketServer({ server, - handleProtocols: (protocols) => [...protocols].join(', ') + handleProtocols: (protocols) => protocols.values().next().value }) wss.on('connection', (ws, request) => { From 6bb0c9bb500ced072d91d8e501188d3a7cb46d18 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 25 Feb 2024 21:42:59 -0500 Subject: [PATCH 082/123] unite webidl stringification (#2843) * unite webidl stringification * fixup * fixup * fixup * fixup * fixup --- lib/web/fetch/webidl.js | 29 ++++++++++++++++++++++------- test/webidl/converters.js | 23 +++++++++++++++++++++++ test/websocket/messageevent.js | 4 ++-- types/webidl.d.ts | 5 +++++ 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/lib/web/fetch/webidl.js b/lib/web/fetch/webidl.js index f111162a10d..ceb7f3e8b09 100644 --- a/lib/web/fetch/webidl.js +++ b/lib/web/fetch/webidl.js @@ -1,6 +1,6 @@ 'use strict' -const { types } = require('node:util') +const { types, inspect } = require('node:util') const { toUSVString } = require('../../core/util') /** @type {import('../../../types/webidl').Webidl} */ @@ -136,7 +136,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { ) { throw webidl.errors.exception({ header: 'Integer conversion', - message: `Could not convert ${V} to an integer.` + message: `Could not convert ${webidl.util.Stringify(V)} to an integer.` }) } @@ -216,6 +216,21 @@ webidl.util.IntegerPart = function (n) { return r } +webidl.util.Stringify = function (V) { + const type = webidl.util.Type(V) + + switch (type) { + case 'Symbol': + return `Symbol(${V.description})` + case 'Object': + return inspect(V) + case 'String': + return `"${V}"` + default: + return `${V}` + } +} + // https://webidl.spec.whatwg.org/#es-sequence webidl.sequenceConverter = function (converter) { return (V, Iterable) => { @@ -324,7 +339,7 @@ webidl.interfaceConverter = function (i) { if (opts.strict !== false && !(V instanceof i)) { throw webidl.errors.exception({ header: i.name, - message: `Expected ${V} to be an instance of ${i.name}.` + message: `Expected ${webidl.util.Stringify(V)} to be an instance of ${i.name}.` }) } @@ -515,8 +530,8 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { !types.isAnyArrayBuffer(V) ) { throw webidl.errors.conversionFailed({ - prefix: `${V}`, - argument: `${V}`, + prefix: webidl.util.Stringify(V), + argument: webidl.util.Stringify(V), types: ['ArrayBuffer'] }) } @@ -561,7 +576,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { ) { throw webidl.errors.conversionFailed({ prefix: `${T.name}`, - argument: `${V}`, + argument: webidl.util.Stringify(V), types: [T.name] }) } @@ -644,7 +659,7 @@ webidl.converters.BufferSource = function (V, opts = {}) { return webidl.converters.DataView(V, opts, { ...opts, allowShared: false }) } - throw new TypeError(`Could not convert ${V} to a BufferSource.`) + throw new TypeError(`Could not convert ${webidl.util.Stringify(V)} to a BufferSource.`) } webidl.converters['sequence'] = webidl.sequenceConverter( diff --git a/test/webidl/converters.js b/test/webidl/converters.js index 0e906ed6719..f39c05a8d7b 100644 --- a/test/webidl/converters.js +++ b/test/webidl/converters.js @@ -183,3 +183,26 @@ test('ByteString', () => { 'index 7 has a value of 256 which is greater than 255.' }) }) + +test('webidl.util.Stringify', (t) => { + const circular = {} + circular.circular = circular + + const pairs = [ + [Object.create(null), '[Object: null prototype] {}'], + [{ a: 'b' }, "{ a: 'b' }"], + [Symbol('sym'), 'Symbol(sym)'], + [Symbol.iterator, 'Symbol(Symbol.iterator)'], // well-known symbol + [true, 'true'], + [0, '0'], + ['hello', '"hello"'], + ['', '""'], + [null, 'null'], + [undefined, 'undefined'], + [circular, ' { circular: [Circular *1] }'] + ] + + for (const [value, expected] of pairs) { + assert.deepStrictEqual(webidl.util.Stringify(value), expected) + } +}) diff --git a/test/websocket/messageevent.js b/test/websocket/messageevent.js index e1ab3d16dd7..de3b6c5fc8e 100644 --- a/test/websocket/messageevent.js +++ b/test/websocket/messageevent.js @@ -103,7 +103,7 @@ test('test/parallel/test-worker-message-port.js', () => { }) assert.throws(() => new MessageEvent('message', { source: {} }), { constructor: TypeError, - message: 'MessagePort: Expected [object Object] to be an instance of MessagePort.' + message: 'MessagePort: Expected {} to be an instance of MessagePort.' }) assert.throws(() => new MessageEvent('message', { ports: 0 }), { constructor: TypeError, @@ -117,7 +117,7 @@ test('test/parallel/test-worker-message-port.js', () => { new MessageEvent('message', { ports: [{}] }) , { constructor: TypeError, - message: 'MessagePort: Expected [object Object] to be an instance of MessagePort.' + message: 'MessagePort: Expected {} to be an instance of MessagePort.' }) assert(new MessageEvent('message') instanceof Event) diff --git a/types/webidl.d.ts b/types/webidl.d.ts index f29bebbb1e8..1e362d6f40d 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -62,6 +62,11 @@ interface WebidlUtil { * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint */ IntegerPart (N: number): number + + /** + * Stringifies {@param V} + */ + Stringify (V: any): string } interface WebidlConverters { From 334adc06661f1e715322f64e9a5340cf15f3760a Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 26 Feb 2024 08:26:20 +0100 Subject: [PATCH 083/123] fix: deflake connect-timeout test (#2851) --- test/connect-timeout.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index 1378de82cf5..98f7c466bbf 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -5,25 +5,26 @@ const { test, after, describe } = require('node:test') const { Client, Pool, errors } = require('..') const net = require('node:net') const assert = require('node:assert') -const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(ms)) // Using describe instead of test to avoid the timeout -describe('prioritize socket errors over timeouts', () => { +describe('prioritize socket errors over timeouts', async () => { const t = tspl({ ...assert, after: () => {} }, { plan: 1 }) - const connectTimeout = 1000 - const client = new Pool('http://foobar.bar:1234', { connectTimeout: 2 }) + const client = new Pool('http://foobar.bar:1234', { connectTimeout: 1 }) client.request({ method: 'GET', path: '/foobar' }) .then(() => t.fail()) .catch((err) => { - t.strictEqual(['ENOTFOUND', 'EAI_AGAIN'].includes(err.code), true) + t.strictEqual(err.code !== 'UND_ERR_CONNECT_TIMEOUT', true) }) - // block for 1s which is enough for the dns lookup to complete and TO to fire - sleep(connectTimeout) + // block for 1s which is enough for the dns lookup to complete and the + // Timeout to fire + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(1000)) + + await t.completed }) -// never connect +// mock net.connect to avoid the dns lookup net.connect = function (options) { return new net.Socket(options) } From 8b131abbf03ca6249f466f40158d4b412cd9794d Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 26 Feb 2024 11:25:49 +0100 Subject: [PATCH 084/123] fix: coverage reporting (#2763) --- .c8rc.json | 13 +++++++ .github/workflows/nodejs.yml | 73 ++++++++++++++++++++++++++++-------- .gitignore | 3 ++ package.json | 30 +++++++++------ scripts/clean-coverage.js | 15 ++++++++ scripts/platform-shell.js | 12 ++++++ 6 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 .c8rc.json create mode 100644 scripts/clean-coverage.js create mode 100644 scripts/platform-shell.js diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 00000000000..11549ad3fac --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,13 @@ +{ + "all": true, + "reporter": [ + "lcov", + "text", + "html", + "text-summary" + ], + "include": [ + "lib/**/*.js", + "index.js" + ] +} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8d1f63e9398..9583567a00c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,9 +1,9 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - - name: Node CI +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + on: push: branches: @@ -14,20 +14,63 @@ on: pull_request: jobs: - build: - name: Test - uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1 - with: - runs-on: ubuntu-latest, windows-latest - test-command: npm run coverage:ci - timeout-minutes: 15 - post-test-steps: | - - name: Coverage Report - uses: codecov/codecov-action@v4 + test: + timeout-minutes: 15 + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: + - 18 + - 20 + - 21 + runs-on: + - ubuntu-latest + - windows-latest + + runs-on: ${{ matrix.runs-on }} + + steps: + + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js@${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Print version information + run: | + echo OS: $(node -p "os.version()") + echo Node.js: $(node --version) + echo npm: $(npm --version) + echo git: $(git --version) + + - name: Install dependencies + run: npm install + + - name: Print installed dependencies + run: npm ls --all + continue-on-error: true + + - name: Run tests + run: npm run coverage:ci + env: + CI: true + NODE_V8_COVERAGE: ./coverage/tmp + + - name: Coverage Report + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + automerge: if: > github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' - needs: build + needs: test runs-on: ubuntu-latest permissions: pull-requests: write diff --git a/.gitignore b/.gitignore index acd1b69eca2..0d8d333f83a 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ fuzz-results-*.json # Bundle output undici-fetch.js /test/imports/undici-import.js + +# .npmrc has platform specific value for windows +.npmrc diff --git a/package.json b/package.json index 9cd9f7431cc..bc1d2f6520c 100644 --- a/package.json +++ b/package.json @@ -67,24 +67,28 @@ "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", - "test": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript && npm run test:node-test", - "test:cookies": "borp --coverage -p \"test/cookie/*.js\"", - "test:node-fetch": "borp --coverage -p \"test/node-fetch/**/*.js\"", - "test:eventsource": "npm run build:node && borp --expose-gc --coverage -p \"test/eventsource/*.js\"", - "test:fetch": "npm run build:node && borp --expose-gc --coverage -p \"test/fetch/*.js\" && borp --coverage -p \"test/webidl/*.js\"", - "test:jest": "jest", + "test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript", + "test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest", + "test:cookies": "borp -p \"test/cookie/*.js\"", + "test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"", + "test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"", + "test:fetch": "npm run build:node && borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\"", + "test:jest": "cross-env NODE_V8_COVERAGE= jest", "test:unit": "borp --expose-gc -p \"test/*.js\"", - "test:node-test": "borp --coverage -p \"test/node-test/**/*.js\"", - "test:tdd": "borp --coverage --expose-gc -p \"test/*.js\"", + "test:node-test": "borp -p \"test/node-test/**/*.js\"", + "test:tdd": "borp --expose-gc -p \"test/*.js\"", "test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w", "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts", - "test:websocket": "borp --coverage -p \"test/websocket/*.js\"", + "test:websocket": "borp -p \"test/websocket/*.js\"", "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", - "coverage": "nyc --reporter=text --reporter=html npm run test", - "coverage:ci": "nyc --reporter=lcov npm run test", + "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test && npm run coverage:report", + "coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test && npm run coverage:report:ci", + "coverage:clean": "node ./scripts/clean-coverage.js", + "coverage:report": "cross-env NODE_V8_COVERAGE= c8 report", + "coverage:report:ci": "c8 report", "bench": "echo \"Error: Benchmarks have been moved to '\/benchmarks'\" && exit 1", "serve:website": "echo \"Error: Documentation has been moved to '\/docs'\" && exit 1", - "prepare": "husky install", + "prepare": "husky install && node ./scripts/platform-shell.js", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, "devDependencies": { @@ -93,6 +97,8 @@ "@types/node": "^18.0.3", "abort-controller": "^3.0.0", "borp": "^0.9.1", + "c8": "^9.1.0", + "cross-env": "^7.0.3", "dns-packet": "^5.4.0", "form-data": "^4.0.0", "formdata-node": "^6.0.3", diff --git a/scripts/clean-coverage.js b/scripts/clean-coverage.js new file mode 100644 index 00000000000..0be16871d03 --- /dev/null +++ b/scripts/clean-coverage.js @@ -0,0 +1,15 @@ +'use strict' + +const { rmSync } = require('node:fs') +const { resolve } = require('node:path') + +if (process.env.NODE_V8_COVERAGE) { + if (process.env.NODE_V8_COVERAGE.endsWith('/tmp')) { + rmSync(resolve(__dirname, process.env.NODE_V8_COVERAGE, '..'), { recursive: true, force: true }) + } else { + rmSync(resolve(__dirname, process.env.NODE_V8_COVERAGE), { recursive: true, force: true }) + } +} else { + console.log(resolve(__dirname, 'coverage')) + rmSync(resolve(__dirname, '../coverage'), { recursive: true, force: true }) +} diff --git a/scripts/platform-shell.js b/scripts/platform-shell.js new file mode 100644 index 00000000000..093ce536a77 --- /dev/null +++ b/scripts/platform-shell.js @@ -0,0 +1,12 @@ +'use strict' + +const { platform } = require('node:os') +const { writeFileSync } = require('node:fs') +const { resolve } = require('node:path') + +if (platform() === 'win32') { + writeFileSync( + resolve(__dirname, '.npmrc'), + 'script-shell = "C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"\n' + ) +} From 16e49ff7e6e64114df9087b50d85302019182043 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 26 Feb 2024 11:30:05 +0100 Subject: [PATCH 085/123] fix: pipelining logic is not relevant for h2 (#2850) --- lib/dispatcher/client.js | 81 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index b4ca2f85428..6e90109ca50 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -457,21 +457,10 @@ function onHTTP2GoAway (code) { client[kSocket] = null client[kHTTP2Session] = null - if (client.destroyed) { - assert(this[kPending] === 0) - - // Fail entire queue. - const requests = client[kQueue].splice(client[kRunningIdx]) - for (let i = 0; i < requests.length; i++) { - const request = requests[i] - errorRequest(this, request, err) - } - } else if (client[kRunning] > 0) { - // Fail head of pipeline. - const request = client[kQueue][client[kRunningIdx]] - client[kQueue][client[kRunningIdx]++] = null - - errorRequest(client, request, err) + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) } client[kPendingIdx] = client[kRunningIdx] @@ -1089,7 +1078,9 @@ function onSocketClose () { client[kSocket] = null - if (client.destroyed) { + // TODO (fix): Always fail entire queue + + if (client.destroyed || client[kHTTPConnVersion] === 'h2') { assert(client[kPending] === 0) // Fail entire queue. @@ -1325,8 +1316,10 @@ function _resume (client, sync) { return } - if (client[kRunning] >= (client[kPipelining] || 1)) { - return + if (client[kHTTPConnVersion] === 'h1') { + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } } const request = client[kQueue][client[kPendingIdx]] @@ -1353,35 +1346,41 @@ function _resume (client, sync) { return } - if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket.destroyed) { return } - if (client[kRunning] > 0 && !request.idempotent) { - // Non-idempotent request cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return - } + if (client[kHTTPConnVersion] === 'h1') { + if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } - if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { - // Don't dispatch an upgrade until all preceding requests have completed. - // A misbehaving server might upgrade the connection before all pipelined - // request has completed. - return - } + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } - if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && - (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { - // Request with stream or iterator body can error while other requests - // are inflight and indirectly error those as well. - // Ensure this doesn't happen by waiting for inflight - // to complete before dispatching. + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } - // Request with stream or iterator body cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } } if (!request.aborted && write(client, request)) { From 4090043ef1a599da9262e6ff422e227db230cbb4 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 27 Feb 2024 00:41:36 -0500 Subject: [PATCH 086/123] processBody doesn't need to return a promise (#2858) --- lib/web/fetch/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index e9733362e95..e7115b2e7e6 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -204,7 +204,7 @@ function fetch (input, init = undefined) { const processResponse = (response) => { // 1. If locallyAborted is true, terminate these substeps. if (locallyAborted) { - return Promise.resolve() + return } // 2. If response’s aborted flag is set, then: @@ -217,14 +217,14 @@ function fetch (input, init = undefined) { // deserializedError. abortFetch(p, request, responseObject, controller.serializedAbortReason) - return Promise.resolve() + return } // 3. If response is a network error, then reject p with a TypeError // and terminate these substeps. if (response.type === 'error') { p.reject(new TypeError('fetch failed', { cause: response.error })) - return Promise.resolve() + return } // 4. Set responseObject to the result of creating a Response object, From 0070ea189901fae18ef0fe4fcef2ae521d749b74 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Tue, 27 Feb 2024 10:51:17 +0100 Subject: [PATCH 087/123] refactor: split client into client-h1/h2 (#2848) --- lib/core/symbols.js | 2 + lib/dispatcher/client-h1.js | 1281 ++++++++++++++++++++++++++ lib/dispatcher/client-h2.js | 605 ++++++++++++ lib/dispatcher/client.js | 1723 +---------------------------------- 4 files changed, 1903 insertions(+), 1708 deletions(-) create mode 100644 lib/dispatcher/client-h1.js create mode 100644 lib/dispatcher/client-h2.js diff --git a/lib/core/symbols.js b/lib/core/symbols.js index d3edb65dda6..1eb4858c378 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -33,6 +33,8 @@ module.exports = { kNeedDrain: Symbol('need drain'), kReset: Symbol('reset'), kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kResume: Symbol('resume'), + kOnError: Symbol('on error'), kMaxHeadersSize: Symbol('max headers size'), kRunningIdx: Symbol('running index'), kPendingIdx: Symbol('pending index'), diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js new file mode 100644 index 00000000000..9ee1686efd9 --- /dev/null +++ b/lib/dispatcher/client-h1.js @@ -0,0 +1,1281 @@ +'use strict' + +/* global WebAssembly */ + +const assert = require('node:assert') +const util = require('../core/util.js') +const { channels } = require('../core/diagnostics.js') +const timers = require('../util/timers.js') +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError +} = require('../core/errors.js') +const { + kUrl, + kReset, + kClient, + kParser, + kBlocking, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kMaxRequests, + kCounter, + kMaxResponseSize, + kHTTPConnVersion, + kListeners, + kOnError, + kResume +} = require('../core/symbols.js') + +const constants = require('../llhttp/constants.js') +const EMPTY_BUF = Buffer.alloc(0) +const FastBuffer = Buffer[Symbol.species] + +let extractBody + +function addListener (obj, name, listener) { + const listeners = (obj[kListeners] ??= []) + listeners.push([name, listener]) + obj.on(name, listener) + return obj +} + +function removeAllListeners (obj) { + for (const [name, listener] of obj[kListeners] ?? []) { + obj.removeListener(name, listener) + } + obj[kListeners] = null +} + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined + + let mod + try { + mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() +llhttpPromise.catch() + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + timers.clearTimeout(this.timeout) + if (value) { + this.timeout = timers.setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + request.onResponseStarted() + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10) { + const headerName = util.bufferToLowerCasedHeaderName(key) + if (headerName === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (headerName === 'connection') { + this.connection += buf.toString() + } + } else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + + removeAllListeners(socket) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + client[kResume]() + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + client[kResume]() + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(() => client[kResume]()) + } else { + client[kResume]() + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +async function connectH1 (client, socket) { + client[kHTTPConnVersion] = 'h1' + + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + + addListener(socket, 'error', function (err) { + const parser = this[kParser] + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + + this[kError] = err + + this[kClient][kOnError](err) + }) + addListener(socket, 'readable', function () { + const parser = this[kParser] + + if (parser) { + parser.readMore() + } + }) + addListener(socket, 'end', function () { + const parser = this[kParser] + + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) + }) + addListener(socket, 'close', function () { + const client = this[kClient] + const parser = this[kParser] + + if (parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + client[kResume]() + }) +} + +function resumeH1 (client) { + const socket = client[kSocket] + + if (socket && !socket.destroyed) { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function writeH1 (client, request) { + const { method, path, host, upgrade, blocking, reset } = request + + let { body, headers, contentLength } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (util.isFormDataLike(body)) { + if (!extractBody) { + extractBody = require('../web/fetch/body.js').extractBody + } + + const [bodyStream, contentType] = extractBody(body) + if (request.contentType == null) { + headers += `content-type: ${contentType}\r\n` + } + body = bodyStream.stream + contentLength = bodyStream.length + } else if (util.isBlobLike(body) && request.contentType == null && body.type) { + headers += `content-type: ${body.type}\r\n` + } + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + contentLength = bodyLength ?? contentLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + util.destroy(body) + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body || bodyLength === 0) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + const onClose = function () { + // 'close' might be emitted *before* 'error' for + // broken streams. Wait a tick to avoid this case. + queueMicrotask(() => { + // It's only safe to remove 'error' listener after + // 'close'. + body.removeListener('error', onFinished) + }) + + if (!finished) { + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('close', onClose) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onClose) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) + + if (body.errorEmitted ?? body.errored) { + setImmediate(() => onFinished(body.errored)) + } else if (body.endEmitted ?? body.readableEnded) { + setImmediate(() => onFinished(null)) + } + + if (body.closeEmitted ?? body.closed) { + setImmediate(onClose) + } +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + client[kResume]() + } catch (err) { + util.destroy(socket, err) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + client[kResume]() + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +module.exports = { + connectH1, + writeH1, + resumeH1 +} diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js new file mode 100644 index 00000000000..89a42ed273f --- /dev/null +++ b/lib/dispatcher/client-h2.js @@ -0,0 +1,605 @@ +'use strict' + +const assert = require('node:assert') +const { pipeline } = require('node:stream') +const util = require('../core/util.js') +const Request = require('../core/request.js') +const { + RequestContentLengthMismatchError, + RequestAbortedError, + SocketError, + InformationalError +} = require('../core/errors.js') +const { + kUrl, + kReset, + kClient, + kRunning, + kPending, + kQueue, + kPendingIdx, + kRunningIdx, + kError, + kSocket, + kStrictContentLength, + kHTTPConnVersion, + kOnError, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2CopyHeaders, + kResume +} = require('../core/symbols.js') + +// Experimental +let h2ExperimentalWarned = false + +/** @type {import('http2')} */ +let http2 +try { + http2 = require('node:http2') +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +async function connectH2 (client, socket) { + client[kHTTPConnVersion] = 'h2' + + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', function () { + const { [kClient]: client } = this + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + client[kResume]() + }) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + + socket.on('error', function (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kError] = err + + this[kClient][kOnError](err) + }) + socket.on('end', function () { + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) + }) +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + this[kClient][kOnError](err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + this[kClient][kOnError](err) + } +} + +function onHttp2SessionEnd () { + this.destroy(new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + client[kResume]() +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function writeH2 (client, request) { + const session = client[kHTTP2Session] + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + if (request.aborted) { + return false + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_METHOD] = method + + try { + // We are already connected, streams are pending. + // We can call on connect, and wait for abort + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + err = err || new RequestAbortedError() + + if (stream != null) { + util.destroy(stream, err) + + h2State.openStreams -= 1 + if (h2State.openStreams === 0) { + session.unref() + } + } + + errorRequest(client, request, err) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (method === 'CONNECT') { + session.ref() + // We are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omitted when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + request.onResponseStarted() + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + // When state is null, it means we haven't consumed body and the stream still do not have + // a state. + // Present specially when using pipeline or stream + if (stream.state?.state == null || stream.state.state < 6) { + request.onComplete([]) + return + } + + // Stream is closed or half-closed-remote (6), decrement counter and cleanup + // It does not have sense to continue working with the stream as we do not + // have yet RST_STREAM support on client-side + h2State.openStreams -= 1 + if (h2State.openStreams === 0) { + session.unref() + } + + const err = new InformationalError('HTTP/2: stream half-closed (remote)') + errorRequest(client, request, err) + util.destroy(stream, err) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) { + stream.pause() + } + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) { + session.unref() + } + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Support push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + stream.end() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + client[kResume]() + } catch (err) { + util.destroy(h2stream) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + request.onRequestSent() + h2stream.end() + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } +} + +module.exports = { + connectH2, + writeH2 +} diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 6e90109ca50..577dbb7ab56 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -2,29 +2,16 @@ 'use strict' -/* global WebAssembly */ - const assert = require('node:assert') const net = require('node:net') const http = require('node:http') -const { pipeline } = require('node:stream') const util = require('../core/util.js') const { channels } = require('../core/diagnostics.js') -const timers = require('../util/timers.js') const Request = require('../core/request.js') const DispatcherBase = require('./dispatcher-base') const { - RequestContentLengthMismatchError, - ResponseContentLengthMismatchError, InvalidArgumentError, - RequestAbortedError, - HeadersTimeoutError, - HeadersOverflowError, - SocketError, InformationalError, - BodyTimeoutError, - HTTPParserError, - ResponseExceededMaxSizeError, ClientDestroyedError } = require('../core/errors.js') const buildConnector = require('../core/connect.js') @@ -34,7 +21,6 @@ const { kServerName, kClient, kBusy, - kParser, kConnect, kBlocking, kResuming, @@ -46,7 +32,6 @@ const { kConnected, kConnecting, kNeedDrain, - kNoRef, kKeepAliveDefaultTimeout, kHostHeader, kPendingIdx, @@ -72,60 +57,20 @@ const { kLocalAddress, kMaxResponseSize, kHTTPConnVersion, - kListeners, + kOnError, // HTTP2 kHost, kHTTP2Session, kHTTP2SessionState, kHTTP2BuildRequest, - kHTTP2CopyHeaders, - kHTTP1BuildRequest + kHTTP1BuildRequest, + kResume } = require('../core/symbols.js') - -/** @type {import('http2')} */ -let http2 -try { - http2 = require('node:http2') -} catch { - // @ts-ignore - http2 = { constants: {} } -} - -const { - constants: { - HTTP2_HEADER_AUTHORITY, - HTTP2_HEADER_METHOD, - HTTP2_HEADER_PATH, - HTTP2_HEADER_SCHEME, - HTTP2_HEADER_CONTENT_LENGTH, - HTTP2_HEADER_EXPECT, - HTTP2_HEADER_STATUS - } -} = http2 - -// Experimental -let h2ExperimentalWarned = false - -let extractBody - -const FastBuffer = Buffer[Symbol.species] +const { connectH1, writeH1, resumeH1 } = require('./client-h1.js') +const { connectH2, writeH2 } = require('./client-h2.js') const kClosedResolve = Symbol('kClosedResolve') -function addListener (obj, name, listener) { - const listeners = (obj[kListeners] ??= []) - listeners.push([name, listener]) - obj.on(name, listener) - return obj -} - -function removeAllListeners (obj) { - for (const [name, listener] of obj[kListeners] ?? []) { - obj.removeListener(name, listener) - } - obj[kListeners] = null -} - /** * @type {import('../../types/client.js').default} */ @@ -316,6 +261,9 @@ class Client extends DispatcherBase { this[kQueue] = [] this[kRunningIdx] = 0 this[kPendingIdx] = 0 + + this[kResume] = (sync) => resume(this, sync) + this[kOnError] = (err) => onError(this, err) } get pipelining () { @@ -324,7 +272,7 @@ class Client extends DispatcherBase { set pipelining (value) { this[kPipelining] = value - resume(this, true) + this[kResume](true) } get [kPending] () { @@ -373,7 +321,7 @@ class Client extends DispatcherBase { this[kResuming] = 1 process.nextTick(resume, this) } else { - resume(this, true) + this[kResume](true) } if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { @@ -409,7 +357,7 @@ class Client extends DispatcherBase { this[kClosedResolve]() this[kClosedResolve] = null } - resolve() + resolve(null) } if (this[kHTTP2Session] != null) { @@ -424,632 +372,12 @@ class Client extends DispatcherBase { queueMicrotask(callback) } - resume(this) + this[kResume]() }) } } -function onHttp2SessionError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - - this[kSocket][kError] = err - - onError(this[kClient], err) -} - -function onHttp2FrameError (type, code, id) { - const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) - - if (id === 0) { - this[kSocket][kError] = err - onError(this[kClient], err) - } -} - -function onHttp2SessionEnd () { - util.destroy(this, new SocketError('other side closed')) - util.destroy(this[kSocket], new SocketError('other side closed')) -} - -function onHTTP2GoAway (code) { - const client = this[kClient] - const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) - client[kSocket] = null - client[kHTTP2Session] = null - - const requests = client[kQueue].splice(client[kRunningIdx]) - for (let i = 0; i < requests.length; i++) { - const request = requests[i] - errorRequest(this, request, err) - } - - client[kPendingIdx] = client[kRunningIdx] - - assert(client[kRunning] === 0) - - client.emit('disconnect', - client[kUrl], - [client], - err - ) - - resume(client) -} - -const constants = require('../llhttp/constants.js') const createRedirectInterceptor = require('../interceptor/redirectInterceptor.js') -const EMPTY_BUF = Buffer.alloc(0) - -async function lazyllhttp () { - const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined - - let mod - try { - mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) - } catch (e) { - /* istanbul ignore next */ - - // We could check if the error was caused by the simd option not - // being enabled, but the occurring of this other error - // * https://github.com/emscripten-core/emscripten/issues/11495 - // got me to remove that check to avoid breaking Node 12. - mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) - } - - return await WebAssembly.instantiate(mod, { - env: { - /* eslint-disable camelcase */ - - wasm_on_url: (p, at, len) => { - /* istanbul ignore next */ - return 0 - }, - wasm_on_status: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + currentBufferRef.byteOffset - return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 - }, - wasm_on_message_begin: (p) => { - assert.strictEqual(currentParser.ptr, p) - return currentParser.onMessageBegin() || 0 - }, - wasm_on_header_field: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + currentBufferRef.byteOffset - return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 - }, - wasm_on_header_value: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + currentBufferRef.byteOffset - return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 - }, - wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { - assert.strictEqual(currentParser.ptr, p) - return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 - }, - wasm_on_body: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + currentBufferRef.byteOffset - return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 - }, - wasm_on_message_complete: (p) => { - assert.strictEqual(currentParser.ptr, p) - return currentParser.onMessageComplete() || 0 - } - - /* eslint-enable camelcase */ - } - }) -} - -let llhttpInstance = null -let llhttpPromise = lazyllhttp() -llhttpPromise.catch() - -let currentParser = null -let currentBufferRef = null -let currentBufferSize = 0 -let currentBufferPtr = null - -const TIMEOUT_HEADERS = 1 -const TIMEOUT_BODY = 2 -const TIMEOUT_IDLE = 3 - -class Parser { - constructor (client, socket, { exports }) { - assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) - - this.llhttp = exports - this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) - this.client = client - this.socket = socket - this.timeout = null - this.timeoutValue = null - this.timeoutType = null - this.statusCode = null - this.statusText = '' - this.upgrade = false - this.headers = [] - this.headersSize = 0 - this.headersMaxSize = client[kMaxHeadersSize] - this.shouldKeepAlive = false - this.paused = false - this.resume = this.resume.bind(this) - - this.bytesRead = 0 - - this.keepAlive = '' - this.contentLength = '' - this.connection = '' - this.maxResponseSize = client[kMaxResponseSize] - } - - setTimeout (value, type) { - this.timeoutType = type - if (value !== this.timeoutValue) { - timers.clearTimeout(this.timeout) - if (value) { - this.timeout = timers.setTimeout(onParserTimeout, value, this) - // istanbul ignore else: only for jest - if (this.timeout.unref) { - this.timeout.unref() - } - } else { - this.timeout = null - } - this.timeoutValue = value - } else if (this.timeout) { - // istanbul ignore else: only for jest - if (this.timeout.refresh) { - this.timeout.refresh() - } - } - } - - resume () { - if (this.socket.destroyed || !this.paused) { - return - } - - assert(this.ptr != null) - assert(currentParser == null) - - this.llhttp.llhttp_resume(this.ptr) - - assert(this.timeoutType === TIMEOUT_BODY) - if (this.timeout) { - // istanbul ignore else: only for jest - if (this.timeout.refresh) { - this.timeout.refresh() - } - } - - this.paused = false - this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. - this.readMore() - } - - readMore () { - while (!this.paused && this.ptr) { - const chunk = this.socket.read() - if (chunk === null) { - break - } - this.execute(chunk) - } - } - - execute (data) { - assert(this.ptr != null) - assert(currentParser == null) - assert(!this.paused) - - const { socket, llhttp } = this - - if (data.length > currentBufferSize) { - if (currentBufferPtr) { - llhttp.free(currentBufferPtr) - } - currentBufferSize = Math.ceil(data.length / 4096) * 4096 - currentBufferPtr = llhttp.malloc(currentBufferSize) - } - - new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) - - // Call `execute` on the wasm parser. - // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, - // and finally the length of bytes to parse. - // The return value is an error code or `constants.ERROR.OK`. - try { - let ret - - try { - currentBufferRef = data - currentParser = this - ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) - /* eslint-disable-next-line no-useless-catch */ - } catch (err) { - /* istanbul ignore next: difficult to make a test case for */ - throw err - } finally { - currentParser = null - currentBufferRef = null - } - - const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr - - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)) - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true - socket.unshift(data.slice(offset)) - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr) - let message = '' - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')' - } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) - } - } catch (err) { - util.destroy(socket, err) - } - } - - destroy () { - assert(this.ptr != null) - assert(currentParser == null) - - this.llhttp.llhttp_free(this.ptr) - this.ptr = null - - timers.clearTimeout(this.timeout) - this.timeout = null - this.timeoutValue = null - this.timeoutType = null - - this.paused = false - } - - onStatus (buf) { - this.statusText = buf.toString() - } - - onMessageBegin () { - const { socket, client } = this - - /* istanbul ignore next: difficult to make a test case for */ - if (socket.destroyed) { - return -1 - } - - const request = client[kQueue][client[kRunningIdx]] - if (!request) { - return -1 - } - request.onResponseStarted() - } - - onHeaderField (buf) { - const len = this.headers.length - - if ((len & 1) === 0) { - this.headers.push(buf) - } else { - this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) - } - - this.trackHeader(buf.length) - } - - onHeaderValue (buf) { - let len = this.headers.length - - if ((len & 1) === 1) { - this.headers.push(buf) - len += 1 - } else { - this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) - } - - const key = this.headers[len - 2] - if (key.length === 10) { - const headerName = util.bufferToLowerCasedHeaderName(key) - if (headerName === 'keep-alive') { - this.keepAlive += buf.toString() - } else if (headerName === 'connection') { - this.connection += buf.toString() - } - } else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') { - this.contentLength += buf.toString() - } - - this.trackHeader(buf.length) - } - - trackHeader (len) { - this.headersSize += len - if (this.headersSize >= this.headersMaxSize) { - util.destroy(this.socket, new HeadersOverflowError()) - } - } - - onUpgrade (head) { - const { upgrade, client, socket, headers, statusCode } = this - - assert(upgrade) - - const request = client[kQueue][client[kRunningIdx]] - assert(request) - - assert(!socket.destroyed) - assert(socket === client[kSocket]) - assert(!this.paused) - assert(request.upgrade || request.method === 'CONNECT') - - this.statusCode = null - this.statusText = '' - this.shouldKeepAlive = null - - assert(this.headers.length % 2 === 0) - this.headers = [] - this.headersSize = 0 - - socket.unshift(head) - - socket[kParser].destroy() - socket[kParser] = null - - socket[kClient] = null - socket[kError] = null - - removeAllListeners(socket) - - client[kSocket] = null - client[kHTTP2Session] = null - client[kQueue][client[kRunningIdx]++] = null - client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) - - try { - request.onUpgrade(statusCode, headers, socket) - } catch (err) { - util.destroy(socket, err) - } - - resume(client) - } - - onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { - const { client, socket, headers, statusText } = this - - /* istanbul ignore next: difficult to make a test case for */ - if (socket.destroyed) { - return -1 - } - - const request = client[kQueue][client[kRunningIdx]] - - /* istanbul ignore next: difficult to make a test case for */ - if (!request) { - return -1 - } - - assert(!this.upgrade) - assert(this.statusCode < 200) - - if (statusCode === 100) { - util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) - return -1 - } - - /* this can only happen if server is misbehaving */ - if (upgrade && !request.upgrade) { - util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) - return -1 - } - - assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) - - this.statusCode = statusCode - this.shouldKeepAlive = ( - shouldKeepAlive || - // Override llhttp value which does not allow keepAlive for HEAD. - (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') - ) - - if (this.statusCode >= 200) { - const bodyTimeout = request.bodyTimeout != null - ? request.bodyTimeout - : client[kBodyTimeout] - this.setTimeout(bodyTimeout, TIMEOUT_BODY) - } else if (this.timeout) { - // istanbul ignore else: only for jest - if (this.timeout.refresh) { - this.timeout.refresh() - } - } - - if (request.method === 'CONNECT') { - assert(client[kRunning] === 1) - this.upgrade = true - return 2 - } - - if (upgrade) { - assert(client[kRunning] === 1) - this.upgrade = true - return 2 - } - - assert(this.headers.length % 2 === 0) - this.headers = [] - this.headersSize = 0 - - if (this.shouldKeepAlive && client[kPipelining]) { - const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null - - if (keepAliveTimeout != null) { - const timeout = Math.min( - keepAliveTimeout - client[kKeepAliveTimeoutThreshold], - client[kKeepAliveMaxTimeout] - ) - if (timeout <= 0) { - socket[kReset] = true - } else { - client[kKeepAliveTimeoutValue] = timeout - } - } else { - client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] - } - } else { - // Stop more requests from being dispatched. - socket[kReset] = true - } - - const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false - - if (request.aborted) { - return -1 - } - - if (request.method === 'HEAD') { - return 1 - } - - if (statusCode < 200) { - return 1 - } - - if (socket[kBlocking]) { - socket[kBlocking] = false - resume(client) - } - - return pause ? constants.ERROR.PAUSED : 0 - } - - onBody (buf) { - const { client, socket, statusCode, maxResponseSize } = this - - if (socket.destroyed) { - return -1 - } - - const request = client[kQueue][client[kRunningIdx]] - assert(request) - - assert.strictEqual(this.timeoutType, TIMEOUT_BODY) - if (this.timeout) { - // istanbul ignore else: only for jest - if (this.timeout.refresh) { - this.timeout.refresh() - } - } - - assert(statusCode >= 200) - - if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { - util.destroy(socket, new ResponseExceededMaxSizeError()) - return -1 - } - - this.bytesRead += buf.length - - if (request.onData(buf) === false) { - return constants.ERROR.PAUSED - } - } - - onMessageComplete () { - const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this - - if (socket.destroyed && (!statusCode || shouldKeepAlive)) { - return -1 - } - - if (upgrade) { - return - } - - const request = client[kQueue][client[kRunningIdx]] - assert(request) - - assert(statusCode >= 100) - - this.statusCode = null - this.statusText = '' - this.bytesRead = 0 - this.contentLength = '' - this.keepAlive = '' - this.connection = '' - - assert(this.headers.length % 2 === 0) - this.headers = [] - this.headersSize = 0 - - if (statusCode < 200) { - return - } - - /* istanbul ignore next: should be handled by llhttp? */ - if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { - util.destroy(socket, new ResponseContentLengthMismatchError()) - return -1 - } - - request.onComplete(headers) - - client[kQueue][client[kRunningIdx]++] = null - - if (socket[kWriting]) { - assert.strictEqual(client[kRunning], 0) - // Response completed before request. - util.destroy(socket, new InformationalError('reset')) - return constants.ERROR.PAUSED - } else if (!shouldKeepAlive) { - util.destroy(socket, new InformationalError('reset')) - return constants.ERROR.PAUSED - } else if (socket[kReset] && client[kRunning] === 0) { - // Destroy socket once all requests have completed. - // The request at the tail of the pipeline is the one - // that requested reset and no further requests should - // have been queued since then. - util.destroy(socket, new InformationalError('reset')) - return constants.ERROR.PAUSED - } else if (client[kPipelining] === 1) { - // We must wait a full event loop cycle to reuse this socket to make sure - // that non-spec compliant servers are not closing the connection even if they - // said they won't. - setImmediate(resume, client) - } else { - resume(client) - } - } -} - -function onParserTimeout (parser) { - const { socket, timeoutType, client } = parser - - /* istanbul ignore else */ - if (timeoutType === TIMEOUT_HEADERS) { - if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { - assert(!parser.paused, 'cannot be paused while waiting for headers') - util.destroy(socket, new HeadersTimeoutError()) - } - } else if (timeoutType === TIMEOUT_BODY) { - if (!parser.paused) { - util.destroy(socket, new BodyTimeoutError()) - } - } else if (timeoutType === TIMEOUT_IDLE) { - assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) - util.destroy(socket, new InformationalError('socket idle timeout')) - } -} function onError (client, err) { if ( @@ -1071,41 +399,6 @@ function onError (client, err) { } } -function onSocketClose () { - const { [kClient]: client } = this - - const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) - - client[kSocket] = null - - // TODO (fix): Always fail entire queue - - if (client.destroyed || client[kHTTPConnVersion] === 'h2') { - assert(client[kPending] === 0) - - // Fail entire queue. - const requests = client[kQueue].splice(client[kRunningIdx]) - for (let i = 0; i < requests.length; i++) { - const request = requests[i] - errorRequest(client, request, err) - } - } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { - // Fail head of pipeline. - const request = client[kQueue][client[kRunningIdx]] - client[kQueue][client[kRunningIdx]++] = null - - errorRequest(client, request, err) - } - - client[kPendingIdx] = client[kRunningIdx] - - assert(client[kRunning] === 0) - - client.emit('disconnect', client[kUrl], [client], err) - - resume(client) -} - async function connect (client) { assert(!client[kConnecting]) assert(!client[kSocket]) @@ -1173,8 +466,6 @@ async function connect (client) { await connectH1(client, socket) } - addListener(socket, 'close', onSocketClose) - socket[kCounter] = 0 socket[kMaxRequests] = client[kMaxRequests] socket[kClient] = client @@ -1234,7 +525,7 @@ async function connect (client) { client.emit('connectionError', client[kUrl], [client], err) } - resume(client) + client[kResume]() } function emitDrain (client) { @@ -1274,30 +565,8 @@ function _resume (client, sync) { const socket = client[kSocket] - if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { - if (client[kSize] === 0) { - if (!socket[kNoRef] && socket.unref) { - socket.unref() - socket[kNoRef] = true - } - } else if (socket[kNoRef] && socket.ref) { - socket.ref() - socket[kNoRef] = false - } - - if (client[kSize] === 0) { - if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { - socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) - } - } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { - if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { - const request = client[kQueue][client[kRunningIdx]] - const headersTimeout = request.headersTimeout != null - ? request.headersTimeout - : client[kHeadersTimeout] - socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) - } - } + if (socket && socket.alpnProtocol !== 'h2') { + resumeH1(client) } if (client[kBusy]) { @@ -1391,11 +660,6 @@ function _resume (client, sync) { } } -// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 -function shouldSendContentLength (method) { - return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' -} - function write (client, request) { if (client[kHTTPConnVersion] === 'h2') { // TODO (fix): Why does this not return the value @@ -1406,861 +670,6 @@ function write (client, request) { } } -function writeH1 (client, request) { - const { method, path, host, upgrade, blocking, reset } = request - - let { body, headers, contentLength } = request - - // https://tools.ietf.org/html/rfc7231#section-4.3.1 - // https://tools.ietf.org/html/rfc7231#section-4.3.2 - // https://tools.ietf.org/html/rfc7231#section-4.3.5 - - // Sending a payload body on a request that does not - // expect it can cause undefined behavior on some - // servers and corrupt connection state. Do not - // re-use the connection for further requests. - - const expectsPayload = ( - method === 'PUT' || - method === 'POST' || - method === 'PATCH' - ) - - if (util.isFormDataLike(body)) { - if (!extractBody) { - extractBody = require('../web/fetch/body.js').extractBody - } - - const [bodyStream, contentType] = extractBody(body) - if (request.contentType == null) { - headers += `content-type: ${contentType}\r\n` - } - body = bodyStream.stream - contentLength = bodyStream.length - } else if (util.isBlobLike(body) && request.contentType == null && body.type) { - headers += `content-type: ${body.type}\r\n` - } - - if (body && typeof body.read === 'function') { - // Try to read EOF in order to get length. - body.read(0) - } - - const bodyLength = util.bodyLength(body) - - contentLength = bodyLength ?? contentLength - - if (contentLength === null) { - contentLength = request.contentLength - } - - if (contentLength === 0 && !expectsPayload) { - // https://tools.ietf.org/html/rfc7230#section-3.3.2 - // A user agent SHOULD NOT send a Content-Length header field when - // the request message does not contain a payload body and the method - // semantics do not anticipate such a body. - - contentLength = null - } - - // https://github.com/nodejs/undici/issues/2046 - // A user agent may send a Content-Length header with 0 value, this should be allowed. - if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { - if (client[kStrictContentLength]) { - errorRequest(client, request, new RequestContentLengthMismatchError()) - return false - } - - process.emitWarning(new RequestContentLengthMismatchError()) - } - - const socket = client[kSocket] - - try { - request.onConnect((err) => { - if (request.aborted || request.completed) { - return - } - - errorRequest(client, request, err || new RequestAbortedError()) - - util.destroy(socket, new InformationalError('aborted')) - }) - } catch (err) { - errorRequest(client, request, err) - } - - if (request.aborted) { - util.destroy(body) - return false - } - - if (method === 'HEAD') { - // https://github.com/mcollina/undici/issues/258 - // Close after a HEAD request to interop with misbehaving servers - // that may send a body in the response. - - socket[kReset] = true - } - - if (upgrade || method === 'CONNECT') { - // On CONNECT or upgrade, block pipeline from dispatching further - // requests on this connection. - - socket[kReset] = true - } - - if (reset != null) { - socket[kReset] = reset - } - - if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { - socket[kReset] = true - } - - if (blocking) { - socket[kBlocking] = true - } - - let header = `${method} ${path} HTTP/1.1\r\n` - - if (typeof host === 'string') { - header += `host: ${host}\r\n` - } else { - header += client[kHostHeader] - } - - if (upgrade) { - header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` - } else if (client[kPipelining] && !socket[kReset]) { - header += 'connection: keep-alive\r\n' - } else { - header += 'connection: close\r\n' - } - - if (headers) { - header += headers - } - - if (channels.sendHeaders.hasSubscribers) { - channels.sendHeaders.publish({ request, headers: header, socket }) - } - - /* istanbul ignore else: assertion */ - if (!body || bodyLength === 0) { - if (contentLength === 0) { - socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') - } else { - assert(contentLength === null, 'no body must not have content length') - socket.write(`${header}\r\n`, 'latin1') - } - request.onRequestSent() - } else if (util.isBuffer(body)) { - assert(contentLength === body.byteLength, 'buffer body must have content length') - - socket.cork() - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') - socket.write(body) - socket.uncork() - request.onBodySent(body) - request.onRequestSent() - if (!expectsPayload) { - socket[kReset] = true - } - } else if (util.isBlobLike(body)) { - if (typeof body.stream === 'function') { - writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) - } else { - writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) - } - } else if (util.isStream(body)) { - writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) - } else if (util.isIterable(body)) { - writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) - } else { - assert(false) - } - - return true -} - -function writeH2 (client, request) { - const session = client[kHTTP2Session] - const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request - - let headers - if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) - else headers = reqHeaders - - if (upgrade) { - errorRequest(client, request, new Error('Upgrade not supported for H2')) - return false - } - - if (request.aborted) { - return false - } - - /** @type {import('node:http2').ClientHttp2Stream} */ - let stream - const h2State = client[kHTTP2SessionState] - - headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] - headers[HTTP2_HEADER_METHOD] = method - - try { - // We are already connected, streams are pending. - // We can call on connect, and wait for abort - request.onConnect((err) => { - if (request.aborted || request.completed) { - return - } - - err = err || new RequestAbortedError() - - if (stream != null) { - util.destroy(stream, err) - - h2State.openStreams -= 1 - if (h2State.openStreams === 0) { - session.unref() - } - } - - errorRequest(client, request, err) - }) - } catch (err) { - errorRequest(client, request, err) - } - - if (method === 'CONNECT') { - session.ref() - // We are already connected, streams are pending, first request - // will create a new stream. We trigger a request to create the stream and wait until - // `ready` event is triggered - // We disabled endStream to allow the user to write to the stream - stream = session.request(headers, { endStream: false, signal }) - - if (stream.id && !stream.pending) { - request.onUpgrade(null, null, stream) - ++h2State.openStreams - } else { - stream.once('ready', () => { - request.onUpgrade(null, null, stream) - ++h2State.openStreams - }) - } - - stream.once('close', () => { - h2State.openStreams -= 1 - // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) session.unref() - }) - - return true - } - - // https://tools.ietf.org/html/rfc7540#section-8.3 - // :path and :scheme headers must be omitted when sending CONNECT - - headers[HTTP2_HEADER_PATH] = path - headers[HTTP2_HEADER_SCHEME] = 'https' - - // https://tools.ietf.org/html/rfc7231#section-4.3.1 - // https://tools.ietf.org/html/rfc7231#section-4.3.2 - // https://tools.ietf.org/html/rfc7231#section-4.3.5 - - // Sending a payload body on a request that does not - // expect it can cause undefined behavior on some - // servers and corrupt connection state. Do not - // re-use the connection for further requests. - - const expectsPayload = ( - method === 'PUT' || - method === 'POST' || - method === 'PATCH' - ) - - if (body && typeof body.read === 'function') { - // Try to read EOF in order to get length. - body.read(0) - } - - let contentLength = util.bodyLength(body) - - if (contentLength == null) { - contentLength = request.contentLength - } - - if (contentLength === 0 || !expectsPayload) { - // https://tools.ietf.org/html/rfc7230#section-3.3.2 - // A user agent SHOULD NOT send a Content-Length header field when - // the request message does not contain a payload body and the method - // semantics do not anticipate such a body. - - contentLength = null - } - - // https://github.com/nodejs/undici/issues/2046 - // A user agent may send a Content-Length header with 0 value, this should be allowed. - if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { - if (client[kStrictContentLength]) { - errorRequest(client, request, new RequestContentLengthMismatchError()) - return false - } - - process.emitWarning(new RequestContentLengthMismatchError()) - } - - if (contentLength != null) { - assert(body, 'no body must not have content length') - headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` - } - - session.ref() - - const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null - if (expectContinue) { - headers[HTTP2_HEADER_EXPECT] = '100-continue' - stream = session.request(headers, { endStream: shouldEndStream, signal }) - - stream.once('continue', writeBodyH2) - } else { - stream = session.request(headers, { - endStream: shouldEndStream, - signal - }) - writeBodyH2() - } - - // Increment counter as we have new several streams open - ++h2State.openStreams - - stream.once('response', headers => { - const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers - request.onResponseStarted() - - if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { - stream.pause() - } - }) - - stream.once('end', () => { - // When state is null, it means we haven't consumed body and the stream still do not have - // a state. - // Present specially when using pipeline or stream - if (stream.state?.state == null || stream.state.state < 6) { - request.onComplete([]) - return - } - - // Stream is closed or half-closed-remote (6), decrement counter and cleanup - // It does not have sense to continue working with the stream as we do not - // have yet RST_STREAM support on client-side - h2State.openStreams -= 1 - if (h2State.openStreams === 0) { - session.unref() - } - - const err = new InformationalError('HTTP/2: stream half-closed (remote)') - errorRequest(client, request, err) - util.destroy(stream, err) - }) - - stream.on('data', (chunk) => { - if (request.onData(chunk) === false) { - stream.pause() - } - }) - - stream.once('close', () => { - h2State.openStreams -= 1 - // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) { - session.unref() - } - }) - - stream.once('error', function (err) { - if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { - h2State.streams -= 1 - util.destroy(stream, err) - } - }) - - stream.once('frameError', (type, code) => { - const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) - errorRequest(client, request, err) - - if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { - h2State.streams -= 1 - util.destroy(stream, err) - } - }) - - // stream.on('aborted', () => { - // // TODO(HTTP/2): Support aborted - // }) - - // stream.on('timeout', () => { - // // TODO(HTTP/2): Support timeout - // }) - - // stream.on('push', headers => { - // // TODO(HTTP/2): Support push - // }) - - // stream.on('trailers', headers => { - // // TODO(HTTP/2): Support trailers - // }) - - return true - - function writeBodyH2 () { - /* istanbul ignore else: assertion */ - if (!body) { - request.onRequestSent() - } else if (util.isBuffer(body)) { - assert(contentLength === body.byteLength, 'buffer body must have content length') - stream.cork() - stream.write(body) - stream.uncork() - stream.end() - request.onBodySent(body) - request.onRequestSent() - } else if (util.isBlobLike(body)) { - if (typeof body.stream === 'function') { - writeIterable({ - client, - request, - contentLength, - h2stream: stream, - expectsPayload, - body: body.stream(), - socket: client[kSocket], - header: '' - }) - } else { - writeBlob({ - body, - client, - request, - contentLength, - expectsPayload, - h2stream: stream, - header: '', - socket: client[kSocket] - }) - } - } else if (util.isStream(body)) { - writeStream({ - body, - client, - request, - contentLength, - expectsPayload, - socket: client[kSocket], - h2stream: stream, - header: '' - }) - } else if (util.isIterable(body)) { - writeIterable({ - body, - client, - request, - contentLength, - expectsPayload, - header: '', - h2stream: stream, - socket: client[kSocket] - }) - } else { - assert(false) - } - } -} - -function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') - - if (client[kHTTPConnVersion] === 'h2') { - // For HTTP/2, is enough to pipe the stream - const pipe = pipeline( - body, - h2stream, - (err) => { - if (err) { - util.destroy(body, err) - util.destroy(h2stream, err) - } else { - request.onRequestSent() - } - } - ) - - pipe.on('data', onPipeData) - pipe.once('end', () => { - pipe.removeListener('data', onPipeData) - util.destroy(pipe) - }) - - function onPipeData (chunk) { - request.onBodySent(chunk) - } - - return - } - - let finished = false - - const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) - - const onData = function (chunk) { - if (finished) { - return - } - - try { - if (!writer.write(chunk) && this.pause) { - this.pause() - } - } catch (err) { - util.destroy(this, err) - } - } - const onDrain = function () { - if (finished) { - return - } - - if (body.resume) { - body.resume() - } - } - const onClose = function () { - // 'close' might be emitted *before* 'error' for - // broken streams. Wait a tick to avoid this case. - queueMicrotask(() => { - // It's only safe to remove 'error' listener after - // 'close'. - body.removeListener('error', onFinished) - }) - - if (!finished) { - const err = new RequestAbortedError() - queueMicrotask(() => onFinished(err)) - } - } - const onFinished = function (err) { - if (finished) { - return - } - - finished = true - - assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) - - socket - .off('drain', onDrain) - .off('error', onFinished) - - body - .removeListener('data', onData) - .removeListener('end', onFinished) - .removeListener('close', onClose) - - if (!err) { - try { - writer.end() - } catch (er) { - err = er - } - } - - writer.destroy(err) - - if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { - util.destroy(body, err) - } else { - util.destroy(body) - } - } - - body - .on('data', onData) - .on('end', onFinished) - .on('error', onFinished) - .on('close', onClose) - - if (body.resume) { - body.resume() - } - - socket - .on('drain', onDrain) - .on('error', onFinished) - - if (body.errorEmitted ?? body.errored) { - setImmediate(() => onFinished(body.errored)) - } else if (body.endEmitted ?? body.readableEnded) { - setImmediate(() => onFinished(null)) - } - - if (body.closeEmitted ?? body.closed) { - setImmediate(onClose) - } -} - -async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert(contentLength === body.size, 'blob body must have content length') - - const isH2 = client[kHTTPConnVersion] === 'h2' - try { - if (contentLength != null && contentLength !== body.size) { - throw new RequestContentLengthMismatchError() - } - - const buffer = Buffer.from(await body.arrayBuffer()) - - if (isH2) { - h2stream.cork() - h2stream.write(buffer) - h2stream.uncork() - } else { - socket.cork() - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') - socket.write(buffer) - socket.uncork() - } - - request.onBodySent(buffer) - request.onRequestSent() - - if (!expectsPayload) { - socket[kReset] = true - } - - resume(client) - } catch (err) { - util.destroy(isH2 ? h2stream : socket, err) - } -} - -async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') - - let callback = null - function onDrain () { - if (callback) { - const cb = callback - callback = null - cb() - } - } - - const waitForDrain = () => new Promise((resolve, reject) => { - assert(callback === null) - - if (socket[kError]) { - reject(socket[kError]) - } else { - callback = resolve - } - }) - - if (client[kHTTPConnVersion] === 'h2') { - h2stream - .on('close', onDrain) - .on('drain', onDrain) - - try { - // It's up to the user to somehow abort the async iterable. - for await (const chunk of body) { - if (socket[kError]) { - throw socket[kError] - } - - const res = h2stream.write(chunk) - request.onBodySent(chunk) - if (!res) { - await waitForDrain() - } - } - } catch (err) { - h2stream.destroy(err) - } finally { - request.onRequestSent() - h2stream.end() - h2stream - .off('close', onDrain) - .off('drain', onDrain) - } - - return - } - - socket - .on('close', onDrain) - .on('drain', onDrain) - - const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) - try { - // It's up to the user to somehow abort the async iterable. - for await (const chunk of body) { - if (socket[kError]) { - throw socket[kError] - } - - if (!writer.write(chunk)) { - await waitForDrain() - } - } - - writer.end() - } catch (err) { - writer.destroy(err) - } finally { - socket - .off('close', onDrain) - .off('drain', onDrain) - } -} - -class AsyncWriter { - constructor ({ socket, request, contentLength, client, expectsPayload, header }) { - this.socket = socket - this.request = request - this.contentLength = contentLength - this.client = client - this.bytesWritten = 0 - this.expectsPayload = expectsPayload - this.header = header - - socket[kWriting] = true - } - - write (chunk) { - const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this - - if (socket[kError]) { - throw socket[kError] - } - - if (socket.destroyed) { - return false - } - - const len = Buffer.byteLength(chunk) - if (!len) { - return true - } - - // We should defer writing chunks. - if (contentLength !== null && bytesWritten + len > contentLength) { - if (client[kStrictContentLength]) { - throw new RequestContentLengthMismatchError() - } - - process.emitWarning(new RequestContentLengthMismatchError()) - } - - socket.cork() - - if (bytesWritten === 0) { - if (!expectsPayload) { - socket[kReset] = true - } - - if (contentLength === null) { - socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') - } else { - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') - } - } - - if (contentLength === null) { - socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') - } - - this.bytesWritten += len - - const ret = socket.write(chunk) - - socket.uncork() - - request.onBodySent(chunk) - - if (!ret) { - if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { - // istanbul ignore else: only for jest - if (socket[kParser].timeout.refresh) { - socket[kParser].timeout.refresh() - } - } - } - - return ret - } - - end () { - const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this - request.onRequestSent() - - socket[kWriting] = false - - if (socket[kError]) { - throw socket[kError] - } - - if (socket.destroyed) { - return - } - - if (bytesWritten === 0) { - if (expectsPayload) { - // https://tools.ietf.org/html/rfc7230#section-3.3.2 - // A user agent SHOULD send a Content-Length in a request message when - // no Transfer-Encoding is sent and the request method defines a meaning - // for an enclosed payload body. - - socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') - } else { - socket.write(`${header}\r\n`, 'latin1') - } - } else if (contentLength === null) { - socket.write('\r\n0\r\n\r\n', 'latin1') - } - - if (contentLength !== null && bytesWritten !== contentLength) { - if (client[kStrictContentLength]) { - throw new RequestContentLengthMismatchError() - } else { - process.emitWarning(new RequestContentLengthMismatchError()) - } - } - - if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { - // istanbul ignore else: only for jest - if (socket[kParser].timeout.refresh) { - socket[kParser].timeout.refresh() - } - } - - resume(client) - } - - destroy (err) { - const { socket, client } = this - - socket[kWriting] = false - - if (err) { - assert(client[kRunning] <= 1, 'pipeline should only contain this request') - util.destroy(socket, err) - } - } -} - function errorRequest (client, request, err) { try { request.onError(err) @@ -2270,106 +679,4 @@ function errorRequest (client, request, err) { } } -async function connectH1 (client, socket) { - client[kHTTPConnVersion] = 'h1' - - if (!llhttpInstance) { - llhttpInstance = await llhttpPromise - llhttpPromise = null - } - - socket[kNoRef] = false - socket[kWriting] = false - socket[kReset] = false - socket[kBlocking] = false - socket[kParser] = new Parser(client, socket, llhttpInstance) - - addListener(socket, 'error', function (err) { - const { [kParser]: parser } = this - - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - - // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded - // to the user. - if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() - return - } - - this[kError] = err - - onError(this[kClient], err) - }) - addListener(socket, 'readable', function () { - const { [kParser]: parser } = this - if (parser) { - parser.readMore() - } - }) - addListener(socket, 'end', function () { - const { [kParser]: parser } = this - - if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() - return - } - - util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) - }) - addListener(socket, 'close', function () { - const { [kParser]: parser } = this - - if (parser) { - if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() - } - - this[kParser].destroy() - this[kParser] = null - } - }) -} - -async function connectH2 (client, socket) { - client[kHTTPConnVersion] = 'h2' - - if (!h2ExperimentalWarned) { - h2ExperimentalWarned = true - process.emitWarning('H2 support is experimental, expect them to change at any time.', { - code: 'UNDICI-H2' - }) - } - - const session = http2.connect(client[kUrl], { - createConnection: () => socket, - peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams - }) - - session[kClient] = client - session[kSocket] = socket - session.on('error', onHttp2SessionError) - session.on('frameError', onHttp2FrameError) - session.on('end', onHttp2SessionEnd) - session.on('goaway', onHTTP2GoAway) - session.on('close', onSocketClose) - session.unref() - - client[kHTTP2Session] = session - socket[kHTTP2Session] = session - - addListener(socket, 'error', function (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - - this[kError] = err - - onError(this[kClient], err) - }) - addListener(socket, 'end', function () { - util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) - }) -} - module.exports = Client From 1978601b51c88a5f4cfa3e9de8b8062779058152 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 27 Feb 2024 15:32:38 +0100 Subject: [PATCH 088/123] ci: fix concurrency (#2862) --- .github/workflows/nodejs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 9583567a00c..5baab2742c9 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,7 +1,7 @@ name: Node CI -concurrency: - group: ${{ github.workflow }} +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true on: From 7146587b95ba66756e252528a12bcc96e47a05fa Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 27 Feb 2024 17:37:07 +0100 Subject: [PATCH 089/123] perf: improve performance of isValidSubprotocol (#2861) --- benchmarks/websocketIsValidSubprotocol.mjs | 17 +++++++++ lib/web/websocket/util.js | 42 +++++++++++----------- test/websocket/util.js | 31 ++++++++++++++++ 3 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 benchmarks/websocketIsValidSubprotocol.mjs create mode 100644 test/websocket/util.js diff --git a/benchmarks/websocketIsValidSubprotocol.mjs b/benchmarks/websocketIsValidSubprotocol.mjs new file mode 100644 index 00000000000..d4ab287ba41 --- /dev/null +++ b/benchmarks/websocketIsValidSubprotocol.mjs @@ -0,0 +1,17 @@ +import { bench, group, run } from 'mitata' +import { isValidSubprotocol } from '../lib/web/websocket/util.js' + +const valid = 'valid' +const invalid = 'invalid ' + +group('isValidSubprotocol', () => { + bench(`valid: ${valid}`, () => { + return isValidSubprotocol(valid) + }) + + bench(`invalid: ${invalid}`, () => { + return isValidSubprotocol(invalid) + }) +}) + +await run() diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index dc3e1ad7c3d..abe91be734d 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -119,31 +119,29 @@ function isValidSubprotocol (protocol) { return false } - for (const char of protocol) { - const code = char.charCodeAt(0) + for (let i = 0; i < protocol.length; ++i) { + const code = protocol.charCodeAt(i) if ( - code < 0x21 || + code < 0x21 || // CTL, contains SP (0x20) and HT (0x09) code > 0x7E || - char === '(' || - char === ')' || - char === '<' || - char === '>' || - char === '@' || - char === ',' || - char === ';' || - char === ':' || - char === '\\' || - char === '"' || - char === '/' || - char === '[' || - char === ']' || - char === '?' || - char === '=' || - char === '{' || - char === '}' || - code === 32 || // SP - code === 9 // HT + code === 0x22 || // " + code === 0x28 || // ( + code === 0x29 || // ) + code === 0x2C || // , + code === 0x2F || // / + code === 0x3A || // : + code === 0x3B || // ; + code === 0x3C || // < + code === 0x3D || // = + code === 0x3E || // > + code === 0x3F || // ? + code === 0x40 || // @ + code === 0x5B || // [ + code === 0x5C || // \ + code === 0x5D || // ] + code === 0x7B || // { + code === 0x7D // } ) { return false } diff --git a/test/websocket/util.js b/test/websocket/util.js new file mode 100644 index 00000000000..8dd0062c5cf --- /dev/null +++ b/test/websocket/util.js @@ -0,0 +1,31 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { describe, test } = require('node:test') +const { isValidSubprotocol } = require('../../lib/web/websocket/util') + +describe('isValidSubprotocol', () => { + test('empty string returns false', t => { + t = tspl(t, { plan: 1 }) + t.strictEqual(isValidSubprotocol(''), false) + }) + + test('simple valid value returns false', t => { + t = tspl(t, { plan: 1 }) + t.strictEqual(isValidSubprotocol('chat'), true) + }) + + test('empty string returns false', t => { + t = tspl(t, { plan: 1 }) + t.strictEqual(isValidSubprotocol(''), false) + }) + + test('value with "(),/:;<=>?@[\\]{} returns false', t => { + const chars = '"(),/:;<=>?@[\\]{}' + t = tspl(t, { plan: 17 }) + + for (let i = 0; i < chars.length; ++i) { + t.strictEqual(isValidSubprotocol('valid' + chars[i]), false) + } + }) +}) From 2091dcfa1a9864dab7675be7c6bff34dbde06051 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 27 Feb 2024 18:11:46 +0100 Subject: [PATCH 090/123] perf: reuse TextDecoder instance (#2863) --- lib/web/websocket/receiver.js | 5 +++-- lib/web/websocket/util.js | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 63035618968..18dc474f16e 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -12,6 +12,8 @@ const { WebsocketFrameSend } = require('./frame') // Copyright (c) 2013 Arnout Kazemier and contributors // Copyright (c) 2016 Luigi Pinca and contributors +const textDecoder = new TextDecoder('utf-8', { fatal: true }) + class ByteParser extends Writable { #buffers = [] #byteOffset = 0 @@ -322,8 +324,7 @@ class ByteParser extends Writable { } try { - // TODO: optimize this - reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + reason = textDecoder.decode(reason) } catch { return null } diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index abe91be734d..8abe73c83e3 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -55,6 +55,8 @@ function fireEvent (e, target, eventConstructor = Event, eventInitDict = {}) { target.dispatchEvent(event) } +const textDecoder = new TextDecoder('utf-8', { fatal: true }) + /** * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol * @param {import('./websocket').WebSocket} ws @@ -74,7 +76,7 @@ function websocketMessageReceived (ws, type, data) { // -> type indicates that the data is Text // a new DOMString containing data try { - dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + dataForEvent = textDecoder.decode(data) } catch { failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') return From 5600aa1484faf4adbfd0415889e7d741b8129e3e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 27 Feb 2024 18:51:19 +0100 Subject: [PATCH 091/123] chore: restructure benchmarks (#2864) --- .../{cacheGetFieldValues.mjs => cache/get-field-values.mjs} | 2 +- .../Is-ctl-excluding-htab.mjs} | 2 +- benchmarks/{parseHeaders.mjs => core/parse-headers.mjs} | 2 +- benchmarks/{parseRawHeaders.mjs => core/parse-raw-headers.mjs} | 2 +- benchmarks/{TernarySearchTree.mjs => core/tree.mjs} | 2 +- benchmarks/{ => fetch}/headers-length32.mjs | 2 +- benchmarks/{ => fetch}/headers.mjs | 2 +- .../is-valid-subprotocol.mjs} | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename benchmarks/{cacheGetFieldValues.mjs => cache/get-field-values.mjs} (85%) rename benchmarks/{cookiesIsCTLExcludingHtab.mjs => cookies/Is-ctl-excluding-htab.mjs} (84%) rename benchmarks/{parseHeaders.mjs => core/parse-headers.mjs} (98%) rename benchmarks/{parseRawHeaders.mjs => core/parse-raw-headers.mjs} (93%) rename benchmarks/{TernarySearchTree.mjs => core/tree.mjs} (91%) rename benchmarks/{ => fetch}/headers-length32.mjs (95%) rename benchmarks/{ => fetch}/headers.mjs (96%) rename benchmarks/{websocketIsValidSubprotocol.mjs => websocket/is-valid-subprotocol.mjs} (81%) diff --git a/benchmarks/cacheGetFieldValues.mjs b/benchmarks/cache/get-field-values.mjs similarity index 85% rename from benchmarks/cacheGetFieldValues.mjs rename to benchmarks/cache/get-field-values.mjs index 9a685d24544..c62f7737251 100644 --- a/benchmarks/cacheGetFieldValues.mjs +++ b/benchmarks/cache/get-field-values.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { getFieldValues } from '../lib/web/cache/util.js' +import { getFieldValues } from '../../lib/web/cache/util.js' const values = [ '', diff --git a/benchmarks/cookiesIsCTLExcludingHtab.mjs b/benchmarks/cookies/Is-ctl-excluding-htab.mjs similarity index 84% rename from benchmarks/cookiesIsCTLExcludingHtab.mjs rename to benchmarks/cookies/Is-ctl-excluding-htab.mjs index b0e134b22d2..12cdb901c21 100644 --- a/benchmarks/cookiesIsCTLExcludingHtab.mjs +++ b/benchmarks/cookies/Is-ctl-excluding-htab.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { isCTLExcludingHtab } from '../lib/web/cookies/util.js' +import { isCTLExcludingHtab } from '../../lib/web/cookies/util.js' const valid = 'Space=Cat; Secure; HttpOnly; Max-Age=2' const invalid = 'Space=Cat; Secure; HttpOnly; Max-Age=2\x7F' diff --git a/benchmarks/parseHeaders.mjs b/benchmarks/core/parse-headers.mjs similarity index 98% rename from benchmarks/parseHeaders.mjs rename to benchmarks/core/parse-headers.mjs index 6fb898062b3..439986da93d 100644 --- a/benchmarks/parseHeaders.mjs +++ b/benchmarks/core/parse-headers.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { parseHeaders } from '../lib/core/util.js' +import { parseHeaders } from '../../lib/core/util.js' const target = [ { diff --git a/benchmarks/parseRawHeaders.mjs b/benchmarks/core/parse-raw-headers.mjs similarity index 93% rename from benchmarks/parseRawHeaders.mjs rename to benchmarks/core/parse-raw-headers.mjs index b7ac0f92586..8f789dfc10c 100644 --- a/benchmarks/parseRawHeaders.mjs +++ b/benchmarks/core/parse-raw-headers.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { parseRawHeaders } from '../lib/core/util.js' +import { parseRawHeaders } from '../../lib/core/util.js' const rawHeadersMixed = ['key', 'value', Buffer.from('key'), Buffer.from('value')] const rawHeadersOnlyStrings = ['key', 'value', 'key', 'value'] diff --git a/benchmarks/TernarySearchTree.mjs b/benchmarks/core/tree.mjs similarity index 91% rename from benchmarks/TernarySearchTree.mjs rename to benchmarks/core/tree.mjs index 413288a6af3..1e268e477ea 100644 --- a/benchmarks/TernarySearchTree.mjs +++ b/benchmarks/core/tree.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { tree } from '../lib/core/tree.js' +import { tree } from '../../lib/core/tree.js' const contentLength = Buffer.from('Content-Length') const contentLengthUpperCase = Buffer.from('Content-Length'.toUpperCase()) diff --git a/benchmarks/headers-length32.mjs b/benchmarks/fetch/headers-length32.mjs similarity index 95% rename from benchmarks/headers-length32.mjs rename to benchmarks/fetch/headers-length32.mjs index 9f1c0a906f8..744263f2569 100644 --- a/benchmarks/headers-length32.mjs +++ b/benchmarks/fetch/headers-length32.mjs @@ -1,5 +1,5 @@ import { bench, run } from 'mitata' -import { Headers } from '../lib/web/fetch/headers.js' +import { Headers } from '../../lib/web/fetch/headers.js' const headers = new Headers( [ diff --git a/benchmarks/headers.mjs b/benchmarks/fetch/headers.mjs similarity index 96% rename from benchmarks/headers.mjs rename to benchmarks/fetch/headers.mjs index 8722cf11c1f..d529370c40c 100644 --- a/benchmarks/headers.mjs +++ b/benchmarks/fetch/headers.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { Headers } from '../lib/web/fetch/headers.js' +import { Headers } from '../../lib/web/fetch/headers.js' const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' const charactersLength = characters.length diff --git a/benchmarks/websocketIsValidSubprotocol.mjs b/benchmarks/websocket/is-valid-subprotocol.mjs similarity index 81% rename from benchmarks/websocketIsValidSubprotocol.mjs rename to benchmarks/websocket/is-valid-subprotocol.mjs index d4ab287ba41..8929f1fa8ea 100644 --- a/benchmarks/websocketIsValidSubprotocol.mjs +++ b/benchmarks/websocket/is-valid-subprotocol.mjs @@ -1,5 +1,5 @@ import { bench, group, run } from 'mitata' -import { isValidSubprotocol } from '../lib/web/websocket/util.js' +import { isValidSubprotocol } from '../../lib/web/websocket/util.js' const valid = 'valid' const invalid = 'invalid ' From 44e7ed8a011fa26767fcd82bf402cc4c3ac821e2 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 02:29:47 +0100 Subject: [PATCH 092/123] perf: improve toIMFDate (#2867) --- benchmarks/cookies/to-imf-date.mjs | 12 +++++++++++ lib/web/cookies/util.js | 33 +++++++++++++----------------- test/cookie/to-imf-date.js | 21 +++++++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 benchmarks/cookies/to-imf-date.mjs create mode 100644 test/cookie/to-imf-date.js diff --git a/benchmarks/cookies/to-imf-date.mjs b/benchmarks/cookies/to-imf-date.mjs new file mode 100644 index 00000000000..7934cc74462 --- /dev/null +++ b/benchmarks/cookies/to-imf-date.mjs @@ -0,0 +1,12 @@ +import { bench, group, run } from 'mitata' +import { toIMFDate } from '../../lib/web/cookies/util.js' + +const date = new Date() + +group('toIMFDate', () => { + bench(`toIMFDate: ${date}`, () => { + return toIMFDate(date) + }) +}) + +await run() diff --git a/lib/web/cookies/util.js b/lib/web/cookies/util.js index 6d3a79b69ad..03432431e86 100644 --- a/lib/web/cookies/util.js +++ b/lib/web/cookies/util.js @@ -114,6 +114,18 @@ function validateCookieDomain (domain) { } } +const IMFDays = [ + 'Sun', 'Mon', 'Tue', 'Wed', + 'Thu', 'Fri', 'Sat' +] + +const IMFMonths = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' +] + +const IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2, '0')) + /** * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 * @param {number|Date} date @@ -160,25 +172,7 @@ function toIMFDate (date) { date = new Date(date) } - const days = [ - 'Sun', 'Mon', 'Tue', 'Wed', - 'Thu', 'Fri', 'Sat' - ] - - const months = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - ] - - const dayName = days[date.getUTCDay()] - const day = date.getUTCDate().toString().padStart(2, '0') - const month = months[date.getUTCMonth()] - const year = date.getUTCFullYear() - const hour = date.getUTCHours().toString().padStart(2, '0') - const minute = date.getUTCMinutes().toString().padStart(2, '0') - const second = date.getUTCSeconds().toString().padStart(2, '0') - - return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` + return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT` } /** @@ -287,6 +281,7 @@ function getHeadersList (headers) { module.exports = { isCTLExcludingHtab, + toIMFDate, stringify, getHeadersList } diff --git a/test/cookie/to-imf-date.js b/test/cookie/to-imf-date.js new file mode 100644 index 00000000000..3e48360919d --- /dev/null +++ b/test/cookie/to-imf-date.js @@ -0,0 +1,21 @@ +'use strict' + +const { test, describe } = require('node:test') +const { strictEqual } = require('node:assert') + +const { + toIMFDate +} = require('../../lib/web/cookies/util') + +describe('toIMFDate', () => { + test('should return the same as Date.prototype.toGMTString()', () => { + for (let i = 1; i <= 1e6; i *= 2) { + const date = new Date(i) + strictEqual(toIMFDate(date), date.toGMTString()) + } + for (let i = 0; i <= 1e6; i++) { + const date = new Date(Math.trunc(Math.random() * 8640000000000000)) + strictEqual(toIMFDate(date), date.toGMTString()) + } + }) +}) From c10c3104b295771160fcc47728a633b221f3ccbb Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 05:45:11 +0100 Subject: [PATCH 093/123] cookies: fix validateCookiePath (#2866) --- lib/web/cookies/util.js | 11 ++++-- test/cookie/validate-cookie-path.js | 59 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/cookie/validate-cookie-path.js diff --git a/lib/web/cookies/util.js b/lib/web/cookies/util.js index 03432431e86..b043054d1ce 100644 --- a/lib/web/cookies/util.js +++ b/lib/web/cookies/util.js @@ -90,10 +90,14 @@ function validateCookieValue (value) { * @param {string} path */ function validateCookiePath (path) { - for (const char of path) { - const code = char.charCodeAt(0) + for (let i = 0; i < path.length; ++i) { + const code = path.charCodeAt(i) - if (code < 0x21 || char === ';') { + if ( + code < 0x20 || // exclude CTLs (0-31) + code === 0x7F || // DEL + code === 0x3B // ; + ) { throw new Error('Invalid cookie path') } } @@ -281,6 +285,7 @@ function getHeadersList (headers) { module.exports = { isCTLExcludingHtab, + validateCookiePath, toIMFDate, stringify, getHeadersList diff --git a/test/cookie/validate-cookie-path.js b/test/cookie/validate-cookie-path.js new file mode 100644 index 00000000000..bcca0d2fc87 --- /dev/null +++ b/test/cookie/validate-cookie-path.js @@ -0,0 +1,59 @@ +'use strict' + +const { test, describe } = require('node:test') +const { throws, strictEqual } = require('node:assert') + +const { + validateCookiePath +} = require('../../lib/web/cookies/util') + +describe('validateCookiePath', () => { + test('should throw for CTLs', () => { + throws(() => validateCookiePath('\x00')) + throws(() => validateCookiePath('\x01')) + throws(() => validateCookiePath('\x02')) + throws(() => validateCookiePath('\x03')) + throws(() => validateCookiePath('\x04')) + throws(() => validateCookiePath('\x05')) + throws(() => validateCookiePath('\x06')) + throws(() => validateCookiePath('\x07')) + throws(() => validateCookiePath('\x08')) + throws(() => validateCookiePath('\x09')) + throws(() => validateCookiePath('\x0A')) + throws(() => validateCookiePath('\x0B')) + throws(() => validateCookiePath('\x0C')) + throws(() => validateCookiePath('\x0D')) + throws(() => validateCookiePath('\x0E')) + throws(() => validateCookiePath('\x0F')) + throws(() => validateCookiePath('\x10')) + throws(() => validateCookiePath('\x11')) + throws(() => validateCookiePath('\x12')) + throws(() => validateCookiePath('\x13')) + throws(() => validateCookiePath('\x14')) + throws(() => validateCookiePath('\x15')) + throws(() => validateCookiePath('\x16')) + throws(() => validateCookiePath('\x17')) + throws(() => validateCookiePath('\x18')) + throws(() => validateCookiePath('\x19')) + throws(() => validateCookiePath('\x1A')) + throws(() => validateCookiePath('\x1B')) + throws(() => validateCookiePath('\x1C')) + throws(() => validateCookiePath('\x1D')) + throws(() => validateCookiePath('\x1E')) + throws(() => validateCookiePath('\x1F')) + throws(() => validateCookiePath('\x7F')) + }) + + test('should throw for ; character', () => { + throws(() => validateCookiePath(';')) + }) + + test('should pass for a printable character', t => { + strictEqual(validateCookiePath('A'), undefined) + strictEqual(validateCookiePath('Z'), undefined) + strictEqual(validateCookiePath('a'), undefined) + strictEqual(validateCookiePath('z'), undefined) + strictEqual(validateCookiePath('!'), undefined) + strictEqual(validateCookiePath(' '), undefined) + }) +}) From 19b4d3928c393652d0475fb07735e026009c0698 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 11:17:48 +0100 Subject: [PATCH 094/123] refactor: move out more h2 from core client (#2860) * refactor: move out more h2 from core client * WIP * WIP * WIP --- .gitignore | 2 + lib/core/symbols.js | 5 ++- lib/dispatcher/client-h1.js | 21 +++++++---- lib/dispatcher/client-h2.js | 61 +++++++++++++++++------------- lib/dispatcher/client.js | 75 +++++++++++++------------------------ 5 files changed, 80 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index 0d8d333f83a..60aa663c838 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,5 @@ undici-fetch.js # .npmrc has platform specific value for windows .npmrc + +.tap diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 1eb4858c378..f3c6e1d5339 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -59,8 +59,9 @@ module.exports = { kHTTP2BuildRequest: Symbol('http2 build request'), kHTTP1BuildRequest: Symbol('http1 build request'), kHTTP2CopyHeaders: Symbol('http2 copy headers'), - kHTTPConnVersion: Symbol('http connection version'), kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), kConstruct: Symbol('constructable'), - kListeners: Symbol('listeners') + kListeners: Symbol('listeners'), + kHTTPContext: Symbol('http context'), + kMaxConcurrentStreams: Symbol('max concurrent streams') } diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 9ee1686efd9..3cc9b3123b8 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -47,7 +47,6 @@ const { kMaxRequests, kCounter, kMaxResponseSize, - kHTTPConnVersion, kListeners, kOnError, kResume @@ -644,8 +643,6 @@ function onParserTimeout (parser) { } async function connectH1 (client, socket) { - client[kHTTPConnVersion] = 'h1' - if (!llhttpInstance) { llhttpInstance = await llhttpPromise llhttpPromise = null @@ -735,6 +732,18 @@ async function connectH1 (client, socket) { client[kResume]() }) + + return { + version: 'h1', + write (...args) { + return writeH1(client, ...args) + }, + resume () { + resumeH1(client) + }, + destroy () { + } + } } function resumeH1 (client) { @@ -1274,8 +1283,4 @@ class AsyncWriter { } } -module.exports = { - connectH1, - writeH1, - resumeH1 -} +module.exports = connectH1 diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 89a42ed273f..04d24b741b3 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -22,16 +22,16 @@ const { kError, kSocket, kStrictContentLength, - kHTTPConnVersion, kOnError, // HTTP2 - kHost, + kMaxConcurrentStreams, kHTTP2Session, - kHTTP2SessionState, kHTTP2CopyHeaders, kResume } = require('../core/symbols.js') +const kOpenStreams = Symbol('open streams') + // Experimental let h2ExperimentalWarned = false @@ -57,8 +57,6 @@ const { } = http2 async function connectH2 (client, socket) { - client[kHTTPConnVersion] = 'h2' - if (!h2ExperimentalWarned) { h2ExperimentalWarned = true process.emitWarning('H2 support is experimental, expect them to change at any time.', { @@ -68,9 +66,10 @@ async function connectH2 (client, socket) { const session = http2.connect(client[kUrl], { createConnection: () => socket, - peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + peerMaxConcurrentStreams: client[kMaxConcurrentStreams] }) + session[kOpenStreams] = 0 session[kClient] = client session[kSocket] = socket session.on('error', onHttp2SessionError) @@ -124,6 +123,20 @@ async function connectH2 (client, socket) { socket.on('end', function () { util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) }) + + return { + version: 'h2', + write (...args) { + // TODO (fix): return + writeH2(client, ...args) + }, + resume () { + + }, + destroy (err) { + session.destroy(err) + } + } } function onHttp2SessionError (err) { @@ -217,9 +230,10 @@ function writeH2 (client, request) { /** @type {import('node:http2').ClientHttp2Stream} */ let stream - const h2State = client[kHTTP2SessionState] - headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + const { hostname, port } = client[kUrl] + + headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}` headers[HTTP2_HEADER_METHOD] = method try { @@ -235,8 +249,8 @@ function writeH2 (client, request) { if (stream != null) { util.destroy(stream, err) - h2State.openStreams -= 1 - if (h2State.openStreams === 0) { + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) { session.unref() } } @@ -257,18 +271,18 @@ function writeH2 (client, request) { if (stream.id && !stream.pending) { request.onUpgrade(null, null, stream) - ++h2State.openStreams + ++session[kOpenStreams] } else { stream.once('ready', () => { request.onUpgrade(null, null, stream) - ++h2State.openStreams + ++session[kOpenStreams] }) } stream.once('close', () => { - h2State.openStreams -= 1 + session[kOpenStreams] -= 1 // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) session.unref() + if (session[kOpenStreams] === 0) session.unref() }) return true @@ -348,7 +362,7 @@ function writeH2 (client, request) { } // Increment counter as we have new several streams open - ++h2State.openStreams + ++session[kOpenStreams] stream.once('response', headers => { const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers @@ -371,8 +385,8 @@ function writeH2 (client, request) { // Stream is closed or half-closed-remote (6), decrement counter and cleanup // It does not have sense to continue working with the stream as we do not // have yet RST_STREAM support on client-side - h2State.openStreams -= 1 - if (h2State.openStreams === 0) { + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) { session.unref() } @@ -388,16 +402,16 @@ function writeH2 (client, request) { }) stream.once('close', () => { - h2State.openStreams -= 1 + session[kOpenStreams] -= 1 // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) { + if (session[kOpenStreams] === 0) { session.unref() } }) stream.once('error', function (err) { if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { - h2State.streams -= 1 + session[kOpenStreams] -= 1 util.destroy(stream, err) } }) @@ -407,7 +421,7 @@ function writeH2 (client, request) { errorRequest(client, request, err) if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { - h2State.streams -= 1 + session[kOpenStreams] -= 1 util.destroy(stream, err) } }) @@ -599,7 +613,4 @@ async function writeIterable ({ h2stream, body, client, request, socket, content } } -module.exports = { - connectH2, - writeH2 -} +module.exports = connectH2 diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 577dbb7ab56..2081a4bcd79 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -56,18 +56,16 @@ const { kInterceptors, kLocalAddress, kMaxResponseSize, - kHTTPConnVersion, kOnError, + kHTTPContext, // HTTP2 - kHost, - kHTTP2Session, - kHTTP2SessionState, + kMaxConcurrentStreams, kHTTP2BuildRequest, kHTTP1BuildRequest, kResume } = require('../core/symbols.js') -const { connectH1, writeH1, resumeH1 } = require('./client-h1.js') -const { connectH2, writeH2 } = require('./client-h2.js') +const connectH1 = require('./client-h1.js') +const connectH2 = require('./client-h2.js') const kClosedResolve = Symbol('kClosedResolve') @@ -107,8 +105,8 @@ class Client extends DispatcherBase { autoSelectFamily, autoSelectFamilyAttemptTimeout, // h2 - allowH2, - maxConcurrentStreams + maxConcurrentStreams, + allowH2 } = {}) { super() @@ -236,18 +234,8 @@ class Client extends DispatcherBase { this[kMaxRequests] = maxRequestsPerClient this[kClosedResolve] = null this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 - this[kHTTPConnVersion] = null - - // HTTP/2 - this[kHTTP2Session] = null - this[kHTTP2SessionState] = !allowH2 - ? null - : { - // streams: null, // Fixed queue of streams - For future support of `push` - openStreams: 0, // Keep track of them to decide whether or not unref the session - maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server - } - this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` + this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + this[kHTTPContext] = null // kQueue is built up of 3 sections separated by // the kRunningIdx and kPendingIdx indices. @@ -309,7 +297,9 @@ class Client extends DispatcherBase { [kDispatch] (opts, handler) { const origin = opts.origin || this[kUrl].origin - const request = this[kHTTPConnVersion] === 'h2' + // TODO (fix): Why do these need to be + // TODO (fix): This can happen before connect... + const request = this[kHTTPContext]?.version === 'h2' ? Request[kHTTP2BuildRequest](origin, opts, handler) : Request[kHTTP1BuildRequest](origin, opts, handler) @@ -360,14 +350,13 @@ class Client extends DispatcherBase { resolve(null) } - if (this[kHTTP2Session] != null) { - util.destroy(this[kHTTP2Session], err) - this[kHTTP2Session] = null - this[kHTTP2SessionState] = null + if (this[kHTTPContext] != null) { + this[kHTTPContext].destroy(err) + this[kHTTPContext] = null } if (this[kSocket]) { - util.destroy(this[kSocket].on('close', callback), err) + this[kSocket].destroy(err).on('close', callback) } else { queueMicrotask(callback) } @@ -425,7 +414,7 @@ async function connect (client) { hostname, protocol, port, - version: client[kHTTPConnVersion], + version: client[kHTTPContext]?.version, servername: client[kServerName], localAddress: client[kLocalAddress] }, @@ -460,11 +449,9 @@ async function connect (client) { assert(socket) - if (socket.alpnProtocol === 'h2') { - await connectH2(client, socket) - } else { - await connectH1(client, socket) - } + client[kHTTPContext] = socket.alpnProtocol === 'h2' + ? await connectH2(client, socket) + : await connectH1(client, socket) socket[kCounter] = 0 socket[kMaxRequests] = client[kMaxRequests] @@ -480,7 +467,7 @@ async function connect (client) { hostname, protocol, port, - version: client[kHTTPConnVersion], + version: client[kHTTPContext]?.version, servername: client[kServerName], localAddress: client[kLocalAddress] }, @@ -503,7 +490,7 @@ async function connect (client) { hostname, protocol, port, - version: client[kHTTPConnVersion], + version: client[kHTTPContext]?.version, servername: client[kServerName], localAddress: client[kLocalAddress] }, @@ -565,8 +552,8 @@ function _resume (client, sync) { const socket = client[kSocket] - if (socket && socket.alpnProtocol !== 'h2') { - resumeH1(client) + if (client[kHTTPContext]) { + client[kHTTPContext].resume() } if (client[kBusy]) { @@ -585,7 +572,7 @@ function _resume (client, sync) { return } - if (client[kHTTPConnVersion] === 'h1') { + if (client[kHTTPContext]?.version === 'h1') { if (client[kRunning] >= (client[kPipelining] || 1)) { return } @@ -619,7 +606,7 @@ function _resume (client, sync) { return } - if (client[kHTTPConnVersion] === 'h1') { + if (client[kHTTPContext].version === 'h1') { if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { return } @@ -652,7 +639,7 @@ function _resume (client, sync) { } } - if (!request.aborted && write(client, request)) { + if (!request.aborted && client[kHTTPContext].write(request)) { client[kPendingIdx]++ } else { client[kQueue].splice(client[kPendingIdx], 1) @@ -660,16 +647,6 @@ function _resume (client, sync) { } } -function write (client, request) { - if (client[kHTTPConnVersion] === 'h2') { - // TODO (fix): Why does this not return the value - // from writeH2. - writeH2(client, request) - } else { - return writeH1(client, request) - } -} - function errorRequest (client, request, err) { try { request.onError(err) From 53df078f67e123a2cf66e22a04f2a9dd0d4673f2 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 12:13:09 +0100 Subject: [PATCH 095/123] mock: improve test coverage of buildHeadersFromArray (#2872) --- lib/mock/mock-utils.js | 3 ++- test/mock-agent.js | 9 +++++++-- test/mock-utils.js | 16 +++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 036d39680ff..c8c0ed1eef1 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -358,5 +358,6 @@ module.exports = { buildMockDispatch, checkNetConnect, buildMockOptions, - getHeaderByName + getHeaderByName, + buildHeadersFromArray } diff --git a/test/mock-agent.js b/test/mock-agent.js index ab852896daf..eb58afc6ad1 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2282,7 +2282,7 @@ test('MockAgent - disableNetConnect should throw if dispatch not found by net co }) test('MockAgent - headers function interceptor', async (t) => { - t = tspl(t, { plan: 7 }) + t = tspl(t, { plan: 8 }) const server = createServer((req, res) => { t.fail('should not be called') @@ -2310,7 +2310,7 @@ test('MockAgent - headers function interceptor', async (t) => { t.strictEqual(typeof headers, 'object') return !Object.keys(headers).includes('authorization') } - }).reply(200, 'foo').times(2) + }).reply(200, 'foo').times(3) await t.rejects(request(`${baseUrl}/foo`, { method: 'GET', @@ -2319,6 +2319,11 @@ test('MockAgent - headers function interceptor', async (t) => { } }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + await t.rejects(request(`${baseUrl}/foo`, { + method: 'GET', + headers: ['Authorization', 'Bearer foo'] + }), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + { const { statusCode } = await request(`${baseUrl}/foo`, { method: 'GET', diff --git a/test/mock-utils.js b/test/mock-utils.js index 744011c70b8..f27a6763ae9 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -8,7 +8,8 @@ const { getMockDispatch, getResponseData, getStatusText, - getHeaderByName + getHeaderByName, + buildHeadersFromArray } = require('../lib/mock/mock-utils') test('deleteMockDispatch - should do nothing if not able to find mock dispatch', (t) => { @@ -210,3 +211,16 @@ test('getHeaderByName', (t) => { t.end() }) + +describe('buildHeadersFromArray', () => { + test('it should build headers from array', (t) => { + t = tspl(t, { plan: 2 }) + + const headers = buildHeadersFromArray([ + 'key', 'value' + ]) + + t.deepStrictEqual(Object.keys(headers).length, 1) + t.strictEqual(headers.key, 'value') + }) +}) From dcd44a581f8d2b35b90782484c2ee51726cd0413 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 12:15:44 +0100 Subject: [PATCH 096/123] fix: remove broken build request hack (#2874) --- lib/core/request.js | 125 +++++------------- lib/core/symbols.js | 3 - lib/core/util.js | 1 + lib/dispatcher/client-h1.js | 19 ++- lib/dispatcher/client-h2.js | 24 +++- lib/dispatcher/client.js | 9 +- test/node-test/diagnostics-channel/get.js | 6 +- .../diagnostics-channel/post-stream.js | 4 +- test/node-test/diagnostics-channel/post.js | 6 +- 9 files changed, 79 insertions(+), 118 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 16a1efffe61..dc136408b55 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -5,7 +5,6 @@ const { NotSupportedError } = require('./errors') const assert = require('node:assert') -const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols') const util = require('./util') const { channels } = require('./diagnostics.js') const { headerNameLowerCasedRecord } = require('./constants') @@ -149,7 +148,7 @@ class Request { this.contentType = null - this.headers = '' + this.headers = [] // Only for H2 this.expectContinue = expectContinue != null ? expectContinue : false @@ -310,78 +309,10 @@ class Request { } } - // TODO: adjust to support H2 addHeader (key, value) { processHeader(this, key, value) return this } - - static [kHTTP1BuildRequest] (origin, opts, handler) { - // TODO: Migrate header parsing here, to make Requests - // HTTP agnostic - return new Request(origin, opts, handler) - } - - static [kHTTP2BuildRequest] (origin, opts, handler) { - const headers = opts.headers - opts = { ...opts, headers: null } - - const request = new Request(origin, opts, handler) - - request.headers = {} - - if (Array.isArray(headers)) { - if (headers.length % 2 !== 0) { - throw new InvalidArgumentError('headers array must be even') - } - for (let i = 0; i < headers.length; i += 2) { - processHeader(request, headers[i], headers[i + 1], true) - } - } else if (headers && typeof headers === 'object') { - const keys = Object.keys(headers) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - processHeader(request, key, headers[key], true) - } - } else if (headers != null) { - throw new InvalidArgumentError('headers must be an object or an array') - } - - return request - } - - static [kHTTP2CopyHeaders] (raw) { - const rawHeaders = raw.split('\r\n') - const headers = {} - - for (const header of rawHeaders) { - const [key, value] = header.split(': ') - - if (value == null || value.length === 0) continue - - if (headers[key]) { - headers[key] += `,${value}` - } else { - headers[key] = value - } - } - - return headers - } -} - -function processHeaderValue (key, val, skipAppend) { - if (val && typeof val === 'object') { - throw new InvalidArgumentError(`invalid ${key} header`) - } - - val = val != null ? `${val}` : '' - - if (headerCharRegex.exec(val) !== null) { - throw new InvalidArgumentError(`invalid ${key} header`) - } - - return skipAppend ? val : `${key}: ${val}\r\n` } function processHeader (request, key, val, skipAppend = false) { @@ -400,10 +331,39 @@ function processHeader (request, key, val, skipAppend = false) { } } - if (request.host === null && headerName === 'host') { + if (Array.isArray(val)) { + const arr = [] + for (let i = 0; i < val.length; i++) { + if (typeof val[i] === 'string') { + if (headerCharRegex.exec(val[i]) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(val[i]) + } else if (val[i] === null) { + arr.push('') + } else if (typeof val[i] === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } else { + arr.push(`${val[i]}`) + } + } + val = arr + } else if (typeof val === 'string') { if (headerCharRegex.exec(val) !== null) { throw new InvalidArgumentError(`invalid ${key} header`) } + } else if (val === null) { + val = '' + } else if (typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } else { + val = `${val}` + } + + if (request.host === null && headerName === 'host') { + if (typeof val !== 'string') { + throw new InvalidArgumentError('invalid host header') + } // Consumed by Client request.host = val } else if (request.contentLength === null && headerName === 'content-length') { @@ -413,35 +373,22 @@ function processHeader (request, key, val, skipAppend = false) { } } else if (request.contentType === null && headerName === 'content-type') { request.contentType = val - if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) - else request.headers += processHeaderValue(key, val) + request.headers.push(key, val) } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') { throw new InvalidArgumentError(`invalid ${headerName} header`) } else if (headerName === 'connection') { const value = typeof val === 'string' ? val.toLowerCase() : null if (value !== 'close' && value !== 'keep-alive') { throw new InvalidArgumentError('invalid connection header') - } else if (value === 'close') { + } + + if (value === 'close') { request.reset = true } } else if (headerName === 'expect') { throw new NotSupportedError('expect header not supported') - } else if (Array.isArray(val)) { - for (let i = 0; i < val.length; i++) { - if (skipAppend) { - if (request.headers[key]) { - request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` - } else { - request.headers[key] = processHeaderValue(key, val[i], skipAppend) - } - } else { - request.headers += processHeaderValue(key, val[i]) - } - } - } else if (skipAppend) { - request.headers[key] = processHeaderValue(key, val, skipAppend) } else { - request.headers += processHeaderValue(key, val) + request.headers.push(key, val) } } diff --git a/lib/core/symbols.js b/lib/core/symbols.js index f3c6e1d5339..02fda9e251d 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -56,9 +56,6 @@ module.exports = { kMaxResponseSize: Symbol('max response size'), kHTTP2Session: Symbol('http2Session'), kHTTP2SessionState: Symbol('http2Session state'), - kHTTP2BuildRequest: Symbol('http2 build request'), - kHTTP1BuildRequest: Symbol('http1 build request'), - kHTTP2CopyHeaders: Symbol('http2 copy headers'), kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), kConstruct: Symbol('constructable'), kListeners: Symbol('listeners'), diff --git a/lib/core/util.js b/lib/core/util.js index 96e76cc1355..9b331089b2a 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -516,6 +516,7 @@ kEnumerableProperty.enumerable = true module.exports = { kEnumerableProperty, nop, + isDisturbed, isErrored, isReadable, diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 3cc9b3123b8..df644e0360c 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -817,12 +817,12 @@ function writeH1 (client, request) { const [bodyStream, contentType] = extractBody(body) if (request.contentType == null) { - headers += `content-type: ${contentType}\r\n` + headers.push('content-type', contentType) } body = bodyStream.stream contentLength = bodyStream.length } else if (util.isBlobLike(body) && request.contentType == null && body.type) { - headers += `content-type: ${body.type}\r\n` + headers.push('content-type', body.type) } if (body && typeof body.read === 'function') { @@ -922,8 +922,19 @@ function writeH1 (client, request) { header += 'connection: close\r\n' } - if (headers) { - header += headers + if (Array.isArray(headers)) { + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0] + const val = headers[n + 1] + + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + header += `${key}: ${val[i]}\r\n` + } + } else { + header += `${key}: ${val}\r\n` + } + } } if (channels.sendHeaders.hasSubscribers) { diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 04d24b741b3..5a6cb2ed91b 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -3,7 +3,6 @@ const assert = require('node:assert') const { pipeline } = require('node:stream') const util = require('../core/util.js') -const Request = require('../core/request.js') const { RequestContentLengthMismatchError, RequestAbortedError, @@ -26,7 +25,6 @@ const { // HTTP2 kMaxConcurrentStreams, kHTTP2Session, - kHTTP2CopyHeaders, kResume } = require('../core/symbols.js') @@ -215,10 +213,6 @@ function writeH2 (client, request) { const session = client[kHTTP2Session] const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request - let headers - if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) - else headers = reqHeaders - if (upgrade) { errorRequest(client, request, new Error('Upgrade not supported for H2')) return false @@ -228,6 +222,24 @@ function writeH2 (client, request) { return false } + const headers = {} + for (let n = 0; n < reqHeaders.length; n += 2) { + const key = reqHeaders[n + 0] + const val = reqHeaders[n + 1] + + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (headers[key]) { + headers[key] += `,${val[i]}` + } else { + headers[key] = val[i] + } + } + } else { + headers[key] = val + } + } + /** @type {import('node:http2').ClientHttp2Stream} */ let stream diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 2081a4bcd79..8a4e428f7b4 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -60,8 +60,6 @@ const { kHTTPContext, // HTTP2 kMaxConcurrentStreams, - kHTTP2BuildRequest, - kHTTP1BuildRequest, kResume } = require('../core/symbols.js') const connectH1 = require('./client-h1.js') @@ -296,12 +294,7 @@ class Client extends DispatcherBase { [kDispatch] (opts, handler) { const origin = opts.origin || this[kUrl].origin - - // TODO (fix): Why do these need to be - // TODO (fix): This can happen before connect... - const request = this[kHTTPContext]?.version === 'h2' - ? Request[kHTTP2BuildRequest](origin, opts, handler) - : Request[kHTTP1BuildRequest](origin, opts, handler) + const request = new Request(origin, opts, handler) this[kQueue].push(request) if (this[kResuming]) { diff --git a/test/node-test/diagnostics-channel/get.js b/test/node-test/diagnostics-channel/get.js index 3366481910b..397dfa3bc5f 100644 --- a/test/node-test/diagnostics-channel/get.js +++ b/test/node-test/diagnostics-channel/get.js @@ -32,9 +32,9 @@ test('Diagnostics channel - get', (t) => { assert.equal(request.completed, false) assert.equal(request.method, 'GET') assert.equal(request.path, '/') - assert.equal(request.headers, 'bar: bar\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar']) request.addHeader('hello', 'world') - assert.equal(request.headers, 'bar: bar\r\nhello: world\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) }) let _connector @@ -81,7 +81,7 @@ test('Diagnostics channel - get', (t) => { 'hello: world' ] - assert.equal(headers, expectedHeaders.join('\r\n') + '\r\n') + assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n') }) diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => { diff --git a/test/node-test/diagnostics-channel/post-stream.js b/test/node-test/diagnostics-channel/post-stream.js index 49fa0be1a04..881873a7c1c 100644 --- a/test/node-test/diagnostics-channel/post-stream.js +++ b/test/node-test/diagnostics-channel/post-stream.js @@ -33,9 +33,9 @@ test('Diagnostics channel - post stream', (t) => { assert.equal(request.completed, false) assert.equal(request.method, 'POST') assert.equal(request.path, '/') - assert.equal(request.headers, 'bar: bar\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar']) request.addHeader('hello', 'world') - assert.equal(request.headers, 'bar: bar\r\nhello: world\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) assert.deepStrictEqual(request.body, body) }) diff --git a/test/node-test/diagnostics-channel/post.js b/test/node-test/diagnostics-channel/post.js index cddb22ace17..1408ffbf023 100644 --- a/test/node-test/diagnostics-channel/post.js +++ b/test/node-test/diagnostics-channel/post.js @@ -31,9 +31,9 @@ test('Diagnostics channel - post', (t) => { assert.equal(request.completed, false) assert.equal(request.method, 'POST') assert.equal(request.path, '/') - assert.equal(request.headers, 'bar: bar\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar']) request.addHeader('hello', 'world') - assert.equal(request.headers, 'bar: bar\r\nhello: world\r\n') + assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) assert.deepStrictEqual(request.body, Buffer.from('hello world')) }) @@ -81,7 +81,7 @@ test('Diagnostics channel - post', (t) => { 'hello: world' ] - assert.equal(headers, expectedHeaders.join('\r\n') + '\r\n') + assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n') }) diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => { From 861f7828a3b249889fe764e038d697f7b6538da6 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 12:16:27 +0100 Subject: [PATCH 097/123] fix: remove unnecessary comment --- lib/dispatcher/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 8a4e428f7b4..29dde5cdab3 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -58,7 +58,6 @@ const { kMaxResponseSize, kOnError, kHTTPContext, - // HTTP2 kMaxConcurrentStreams, kResume } = require('../core/symbols.js') From 799aedf59d4298caa06d70117f0c2838902ad6ae Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 12:24:40 +0100 Subject: [PATCH 098/123] chore: filenames should use kebab-case (#2873) --- index.js | 10 +++++----- lib/dispatcher/agent.js | 2 +- lib/dispatcher/client.js | 2 +- lib/dispatcher/retry-agent.js | 2 +- .../{DecoratorHandler.js => decorator-handler.js} | 0 .../{RedirectHandler.js => redirect-handler.js} | 0 lib/handler/{RetryHandler.js => retry-handler.js} | 0 ...{redirectInterceptor.js => redirect-interceptor.js} | 2 +- lib/web/cache/util.js | 2 +- lib/web/cookies/parse.js | 2 +- lib/web/eventsource/eventsource.js | 2 +- lib/web/fetch/body.js | 2 +- lib/web/fetch/{dataURL.js => data-url.js} | 2 ++ lib/web/fetch/file.js | 2 +- lib/web/fetch/index.js | 2 +- lib/web/fetch/request.js | 2 +- lib/web/fetch/response.js | 2 +- lib/web/fetch/util.js | 2 +- lib/web/fileapi/util.js | 2 +- lib/web/websocket/websocket.js | 2 +- test/fetch/data-uri.js | 2 +- test/jest/interceptor.test.js | 2 +- 22 files changed, 24 insertions(+), 22 deletions(-) rename lib/handler/{DecoratorHandler.js => decorator-handler.js} (100%) rename lib/handler/{RedirectHandler.js => redirect-handler.js} (100%) rename lib/handler/{RetryHandler.js => retry-handler.js} (100%) rename lib/interceptor/{redirectInterceptor.js => redirect-interceptor.js} (90%) rename lib/web/fetch/{dataURL.js => data-url.js} (99%) diff --git a/index.js b/index.js index 77fa2b9ffde..f72ea8a540e 100644 --- a/index.js +++ b/index.js @@ -16,11 +16,11 @@ const MockClient = require('./lib/mock/mock-client') const MockAgent = require('./lib/mock/mock-agent') const MockPool = require('./lib/mock/mock-pool') const mockErrors = require('./lib/mock/mock-errors') -const RetryHandler = require('./lib/handler/RetryHandler') +const RetryHandler = require('./lib/handler/retry-handler') const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') -const DecoratorHandler = require('./lib/handler/DecoratorHandler') -const RedirectHandler = require('./lib/handler/RedirectHandler') -const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor') +const DecoratorHandler = require('./lib/handler/decorator-handler') +const RedirectHandler = require('./lib/handler/redirect-handler') +const createRedirectInterceptor = require('./lib/interceptor/redirect-interceptor') Object.assign(Dispatcher.prototype, api) @@ -134,7 +134,7 @@ module.exports.getCookies = getCookies module.exports.getSetCookies = getSetCookies module.exports.setCookie = setCookie -const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/dataURL') +const { parseMIMEType, serializeAMimeType } = require('./lib/web/fetch/data-url') module.exports.parseMIMEType = parseMIMEType module.exports.serializeAMimeType = serializeAMimeType diff --git a/lib/dispatcher/agent.js b/lib/dispatcher/agent.js index d91c412382f..98f1486cac0 100644 --- a/lib/dispatcher/agent.js +++ b/lib/dispatcher/agent.js @@ -6,7 +6,7 @@ const DispatcherBase = require('./dispatcher-base') const Pool = require('./pool') const Client = require('./client') const util = require('../core/util') -const createRedirectInterceptor = require('../interceptor/redirectInterceptor') +const createRedirectInterceptor = require('../interceptor/redirect-interceptor') const kOnConnect = Symbol('onConnect') const kOnDisconnect = Symbol('onDisconnect') diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 29dde5cdab3..e7fb02fb34a 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -358,7 +358,7 @@ class Client extends DispatcherBase { } } -const createRedirectInterceptor = require('../interceptor/redirectInterceptor.js') +const createRedirectInterceptor = require('../interceptor/redirect-interceptor.js') function onError (client, err) { if ( diff --git a/lib/dispatcher/retry-agent.js b/lib/dispatcher/retry-agent.js index 2ca82b0d02c..0c2120d6f26 100644 --- a/lib/dispatcher/retry-agent.js +++ b/lib/dispatcher/retry-agent.js @@ -1,7 +1,7 @@ 'use strict' const Dispatcher = require('./dispatcher') -const RetryHandler = require('../handler/RetryHandler') +const RetryHandler = require('../handler/retry-handler') class RetryAgent extends Dispatcher { #agent = null diff --git a/lib/handler/DecoratorHandler.js b/lib/handler/decorator-handler.js similarity index 100% rename from lib/handler/DecoratorHandler.js rename to lib/handler/decorator-handler.js diff --git a/lib/handler/RedirectHandler.js b/lib/handler/redirect-handler.js similarity index 100% rename from lib/handler/RedirectHandler.js rename to lib/handler/redirect-handler.js diff --git a/lib/handler/RetryHandler.js b/lib/handler/retry-handler.js similarity index 100% rename from lib/handler/RetryHandler.js rename to lib/handler/retry-handler.js diff --git a/lib/interceptor/redirectInterceptor.js b/lib/interceptor/redirect-interceptor.js similarity index 90% rename from lib/interceptor/redirectInterceptor.js rename to lib/interceptor/redirect-interceptor.js index 7cc035e096c..896ee8db939 100644 --- a/lib/interceptor/redirectInterceptor.js +++ b/lib/interceptor/redirect-interceptor.js @@ -1,6 +1,6 @@ 'use strict' -const RedirectHandler = require('../handler/RedirectHandler') +const RedirectHandler = require('../handler/redirect-handler') function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { return (dispatch) => { diff --git a/lib/web/cache/util.js b/lib/web/cache/util.js index d168d45351b..5ac9d846ddc 100644 --- a/lib/web/cache/util.js +++ b/lib/web/cache/util.js @@ -1,7 +1,7 @@ 'use strict' const assert = require('node:assert') -const { URLSerializer } = require('../fetch/dataURL') +const { URLSerializer } = require('../fetch/data-url') const { isValidHeaderName } = require('../fetch/util') /** diff --git a/lib/web/cookies/parse.js b/lib/web/cookies/parse.js index 8d60d1cb69b..3c48c26b93f 100644 --- a/lib/web/cookies/parse.js +++ b/lib/web/cookies/parse.js @@ -2,7 +2,7 @@ const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') const { isCTLExcludingHtab } = require('./util') -const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL') +const { collectASequenceOfCodePointsFast } = require('../fetch/data-url') const assert = require('node:assert') /** diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index 6b34976e31b..cf5093e1bdf 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -6,7 +6,7 @@ const { makeRequest } = require('../fetch/request') const { getGlobalOrigin } = require('../fetch/global') const { webidl } = require('../fetch/webidl') const { EventSourceStream } = require('./eventsource-stream') -const { parseMIMEType } = require('../fetch/dataURL') +const { parseMIMEType } = require('../fetch/data-url') const { MessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') const { delay } = require('./util') diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js index 4b81fcedc42..932df3e6532 100644 --- a/lib/web/fetch/body.js +++ b/lib/web/fetch/body.js @@ -19,7 +19,7 @@ const assert = require('node:assert') const { isErrored } = require('../../core/util') const { isArrayBuffer } = require('node:util/types') const { File: UndiciFile } = require('./file') -const { serializeAMimeType } = require('./dataURL') +const { serializeAMimeType } = require('./data-url') const { Readable } = require('node:stream') /** @type {globalThis['File']} */ diff --git a/lib/web/fetch/dataURL.js b/lib/web/fetch/data-url.js similarity index 99% rename from lib/web/fetch/dataURL.js rename to lib/web/fetch/data-url.js index ee7f72e9c93..a966ace3b8e 100644 --- a/lib/web/fetch/dataURL.js +++ b/lib/web/fetch/data-url.js @@ -1,3 +1,5 @@ +'use strict' + const assert = require('node:assert') const encoder = new TextEncoder() diff --git a/lib/web/fetch/file.js b/lib/web/fetch/file.js index 61a232017b2..e00b58cdb58 100644 --- a/lib/web/fetch/file.js +++ b/lib/web/fetch/file.js @@ -5,7 +5,7 @@ const { types } = require('node:util') const { kState } = require('./symbols') const { isBlobLike } = require('./util') const { webidl } = require('./webidl') -const { parseMIMEType, serializeAMimeType } = require('./dataURL') +const { parseMIMEType, serializeAMimeType } = require('./data-url') const { kEnumerableProperty } = require('../../core/util') const encoder = new TextEncoder() diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index e7115b2e7e6..0ae6a704e11 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -60,7 +60,7 @@ const { const EE = require('node:events') const { Readable, pipeline } = require('node:stream') const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../../core/util') -const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./dataURL') +const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url') const { getGlobalDispatcher } = require('../../global') const { webidl } = require('./webidl') const { STATUS_CODES } = require('node:http') diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index f8759626a1e..afe92499267 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -27,7 +27,7 @@ const { kEnumerableProperty } = util const { kHeaders, kSignal, kState, kGuard, kRealm, kDispatcher } = require('./symbols') const { webidl } = require('./webidl') const { getGlobalOrigin } = require('./global') -const { URLSerializer } = require('./dataURL') +const { URLSerializer } = require('./data-url') const { kHeadersList, kConstruct } = require('../../core/symbols') const assert = require('node:assert') const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('node:events') diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index 355c2847aba..e31f619590f 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -21,7 +21,7 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const { webidl } = require('./webidl') const { FormData } = require('./formdata') const { getGlobalOrigin } = require('./global') -const { URLSerializer } = require('./dataURL') +const { URLSerializer } = require('./data-url') const { kHeadersList, kConstruct } = require('../../core/symbols') const assert = require('node:assert') const { types } = require('node:util') diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index 49ab59e8876..92bcb6cb202 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -4,7 +4,7 @@ const { Transform } = require('node:stream') const zlib = require('node:zlib') const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants') const { getGlobalOrigin } = require('./global') -const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./dataURL') +const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url') const { performance } = require('node:perf_hooks') const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../../core/util') const assert = require('node:assert') diff --git a/lib/web/fileapi/util.js b/lib/web/fileapi/util.js index ce8b10aa2de..9110b872a81 100644 --- a/lib/web/fileapi/util.js +++ b/lib/web/fileapi/util.js @@ -9,7 +9,7 @@ const { } = require('./symbols') const { ProgressEvent } = require('./progressevent') const { getEncoding } = require('./encoding') -const { serializeAMimeType, parseMIMEType } = require('../fetch/dataURL') +const { serializeAMimeType, parseMIMEType } = require('../fetch/data-url') const { types } = require('node:util') const { StringDecoder } = require('string_decoder') const { btoa } = require('node:buffer') diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 0072da48193..c08f02cbe38 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -1,7 +1,7 @@ 'use strict' const { webidl } = require('../fetch/webidl') -const { URLSerializer } = require('../fetch/dataURL') +const { URLSerializer } = require('../fetch/data-url') const { getGlobalOrigin } = require('../fetch/global') const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants') const { diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js index 7c99c8e9422..feab3408d90 100644 --- a/test/fetch/data-uri.js +++ b/test/fetch/data-uri.js @@ -9,7 +9,7 @@ const { stringPercentDecode, parseMIMEType, collectAnHTTPQuotedString -} = require('../../lib/web/fetch/dataURL') +} = require('../../lib/web/fetch/data-url') const { fetch } = require('../..') test('https://url.spec.whatwg.org/#concept-url-serializer', async (t) => { diff --git a/test/jest/interceptor.test.js b/test/jest/interceptor.test.js index 9caa0758936..84e6210fb8d 100644 --- a/test/jest/interceptor.test.js +++ b/test/jest/interceptor.test.js @@ -2,7 +2,7 @@ const { createServer } = require('node:http') const { Agent, request } = require('../../index') -const DecoratorHandler = require('../../lib/handler/DecoratorHandler') +const DecoratorHandler = require('../../lib/handler/decorator-handler') /* global expect */ const defaultOpts = { keepAliveTimeout: 10, keepAliveMaxTimeout: 10 } From 00f1914b617fe6dd9765390277e4c862b6c12a79 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 12:42:27 +0100 Subject: [PATCH 099/123] refactor: split out last h1 specific code from core (#2876) --- lib/dispatcher/client-h1.js | 34 ++++++++++++++++++++++++++++++++++ lib/dispatcher/client-h2.js | 3 +++ lib/dispatcher/client.js | 35 +++++------------------------------ 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index df644e0360c..60d1b511172 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -742,6 +742,40 @@ async function connectH1 (client, socket) { resumeH1(client) }, destroy () { + }, + busy (request) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return true + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return true + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } + + return false } } } diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 5a6cb2ed91b..c0cfd5c88f6 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -133,6 +133,9 @@ async function connectH2 (client, socket) { }, destroy (err) { session.destroy(err) + }, + busy () { + return false } } } diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index e7fb02fb34a..91acddd8371 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -598,37 +598,12 @@ function _resume (client, sync) { return } - if (client[kHTTPContext].version === 'h1') { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { - return - } - - if (client[kRunning] > 0 && !request.idempotent) { - // Non-idempotent request cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return - } - - if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { - // Don't dispatch an upgrade until all preceding requests have completed. - // A misbehaving server might upgrade the connection before all pipelined - // request has completed. - return - } - - if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && - (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { - // Request with stream or iterator body can error while other requests - // are inflight and indirectly error those as well. - // Ensure this doesn't happen by waiting for inflight - // to complete before dispatching. + if (!client[kHTTPContext]) { + return + } - // Request with stream or iterator body cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return - } + if (client[kHTTPContext].busy(request)) { + return } if (!request.aborted && client[kHTTPContext].write(request)) { From a55d61f30cafba9175c0c3ff9474bd7fa3b5561f Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 12:57:40 +0100 Subject: [PATCH 100/123] fix: make pipelining limit work for h2 (#2875) --- lib/dispatcher/client-h1.js | 3 ++- lib/dispatcher/client-h2.js | 1 + lib/dispatcher/client.js | 12 +++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 60d1b511172..0cb6528ae06 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -612,7 +612,7 @@ class Parser { // have been queued since then. util.destroy(socket, new InformationalError('reset')) return constants.ERROR.PAUSED - } else if (client[kPipelining] === 1) { + } else if (client[kPipelining] == null || client[kPipelining] === 1) { // We must wait a full event loop cycle to reuse this socket to make sure // that non-spec compliant servers are not closing the connection even if they // said they won't. @@ -735,6 +735,7 @@ async function connectH1 (client, socket) { return { version: 'h1', + defaultPipelining: 1, write (...args) { return writeH1(client, ...args) }, diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index c0cfd5c88f6..7202c654397 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -124,6 +124,7 @@ async function connectH2 (client, socket) { return { version: 'h2', + defaultPipelining: Infinity, write (...args) { // TODO (fix): return writeH2(client, ...args) diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 91acddd8371..7c75c2ff239 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -66,6 +66,10 @@ const connectH2 = require('./client-h2.js') const kClosedResolve = Symbol('kClosedResolve') +function getPipelining (client) { + return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 +} + /** * @type {import('../../types/client.js').default} */ @@ -280,7 +284,7 @@ class Client extends DispatcherBase { const socket = this[kSocket] return ( (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || - (this[kSize] >= (this[kPipelining] || 1)) || + (this[kSize] >= (getPipelining(this) || 1)) || this[kPending] > 0 ) } @@ -564,10 +568,8 @@ function _resume (client, sync) { return } - if (client[kHTTPContext]?.version === 'h1') { - if (client[kRunning] >= (client[kPipelining] || 1)) { - return - } + if (client[kRunning] >= (getPipelining(client) || 1)) { + return } const request = client[kQueue][client[kPendingIdx]] From 46b1b0f7b957e0f700f5957119b7e748b6526b1f Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 14:51:30 +0100 Subject: [PATCH 101/123] fix: http2 doesn't have pipelining queue (#2878) --- lib/dispatcher/client-h2.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 7202c654397..dcd250254bf 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -81,20 +81,12 @@ async function connectH2 (client, socket) { client[kSocket] = null - if (client.destroyed) { - assert(client[kPending] === 0) - - // Fail entire queue. - const requests = client[kQueue].splice(client[kRunningIdx]) - for (let i = 0; i < requests.length; i++) { - const request = requests[i] - errorRequest(client, request, err) - } - } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { - // Fail head of pipeline. - const request = client[kQueue][client[kRunningIdx]] - client[kQueue][client[kRunningIdx]++] = null + assert(client[kPending] === 0) + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] errorRequest(client, request, err) } From 2a27dc13977dc80c1aabe9cfd3840a9efedb1606 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 14:52:25 +0100 Subject: [PATCH 102/123] fix: minor connect cleanup (#2877) --- lib/dispatcher/client-h1.js | 70 ++++++++++++++++++++++++------------- lib/dispatcher/client-h2.js | 17 ++++++++- lib/dispatcher/client.js | 55 ++++++++++------------------- 3 files changed, 80 insertions(+), 62 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 0cb6528ae06..62a3e29ef24 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -49,7 +49,8 @@ const { kMaxResponseSize, kListeners, kOnError, - kResume + kResume, + kHTTPContext } = require('../core/symbols.js') const constants = require('../llhttp/constants.js') @@ -403,6 +404,7 @@ class Parser { removeAllListeners(socket) client[kSocket] = null + client[kHTTPContext] = null // TODO (fix): This is hacky... client[kQueue][client[kRunningIdx]++] = null client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) @@ -643,6 +645,8 @@ function onParserTimeout (parser) { } async function connectH1 (client, socket) { + client[kSocket] = socket + if (!llhttpInstance) { llhttpInstance = await llhttpPromise llhttpPromise = null @@ -706,6 +710,7 @@ async function connectH1 (client, socket) { const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) client[kSocket] = null + client[kHTTPContext] = null // TODO (fix): This is hacky... if (client.destroyed) { assert(client[kPending] === 0) @@ -733,6 +738,11 @@ async function connectH1 (client, socket) { client[kResume]() }) + let closed = false + socket.on('close', () => { + closed = true + }) + return { version: 'h1', defaultPipelining: 1, @@ -742,38 +752,48 @@ async function connectH1 (client, socket) { resume () { resumeH1(client) }, - destroy () { + destroy (err, callback) { + if (closed) { + queueMicrotask(callback) + } else { + socket.destroy(err).on('close', callback) + } + }, + get destroyed () { + return socket.destroyed }, busy (request) { if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { return true } - if (client[kRunning] > 0 && !request.idempotent) { - // Non-idempotent request cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return true - } - - if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { - // Don't dispatch an upgrade until all preceding requests have completed. - // A misbehaving server might upgrade the connection before all pipelined - // request has completed. - return true - } + if (request) { + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } - if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && - (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { - // Request with stream or iterator body can error while other requests - // are inflight and indirectly error those as well. - // Ensure this doesn't happen by waiting for inflight - // to complete before dispatching. + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return true + } - // Request with stream or iterator body cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - return true + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return true + } } return false diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index dcd250254bf..8155d6e226a 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -55,6 +55,8 @@ const { } = http2 async function connectH2 (client, socket) { + client[kSocket] = socket + if (!h2ExperimentalWarned) { h2ExperimentalWarned = true process.emitWarning('H2 support is experimental, expect them to change at any time.', { @@ -114,6 +116,11 @@ async function connectH2 (client, socket) { util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) }) + let closed = false + socket.on('close', () => { + closed = true + }) + return { version: 'h2', defaultPipelining: Infinity, @@ -124,8 +131,16 @@ async function connectH2 (client, socket) { resume () { }, - destroy (err) { + destroy (err, callback) { session.destroy(err) + if (closed) { + queueMicrotask(callback) + } else { + socket.destroy(err).on('close', callback) + } + }, + get destroyed () { + return socket.destroyed }, busy () { return false diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 7c75c2ff239..8cc334d0baf 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -17,17 +17,14 @@ const { const buildConnector = require('../core/connect.js') const { kUrl, - kReset, kServerName, kClient, kBusy, kConnect, - kBlocking, kResuming, kRunning, kPending, kSize, - kWriting, kQueue, kConnected, kConnecting, @@ -38,7 +35,6 @@ const { kRunningIdx, kError, kPipelining, - kSocket, kKeepAliveTimeoutValue, kMaxHeadersSize, kKeepAliveMaxTimeout, @@ -216,7 +212,6 @@ class Client extends DispatcherBase { : [createRedirectInterceptor({ maxRedirections })] this[kUrl] = util.parseOrigin(url) this[kConnector] = connect - this[kSocket] = null this[kPipelining] = pipelining != null ? pipelining : 1 this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout @@ -277,13 +272,12 @@ class Client extends DispatcherBase { } get [kConnected] () { - return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed } get [kBusy] () { - const socket = this[kSocket] - return ( - (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + return Boolean( + this[kHTTPContext]?.busy(null) || (this[kSize] >= (getPipelining(this) || 1)) || this[kPending] > 0 ) @@ -346,13 +340,9 @@ class Client extends DispatcherBase { resolve(null) } - if (this[kHTTPContext] != null) { - this[kHTTPContext].destroy(err) + if (this[kHTTPContext]) { + this[kHTTPContext].destroy(err, callback) this[kHTTPContext] = null - } - - if (this[kSocket]) { - this[kSocket].destroy(err).on('close', callback) } else { queueMicrotask(callback) } @@ -386,7 +376,7 @@ function onError (client, err) { async function connect (client) { assert(!client[kConnecting]) - assert(!client[kSocket]) + assert(!client[kHTTPContext]) let { host, hostname, protocol, port } = client[kUrl] @@ -441,21 +431,24 @@ async function connect (client) { return } - client[kConnecting] = false - assert(socket) - client[kHTTPContext] = socket.alpnProtocol === 'h2' - ? await connectH2(client, socket) - : await connectH1(client, socket) + try { + client[kHTTPContext] = socket.alpnProtocol === 'h2' + ? await connectH2(client, socket) + : await connectH1(client, socket) + } catch (err) { + socket.destroy().on('error', () => {}) + throw err + } + + client[kConnecting] = false socket[kCounter] = 0 socket[kMaxRequests] = client[kMaxRequests] socket[kClient] = client socket[kError] = null - client[kSocket] = socket - if (channels.connected.hasSubscribers) { channels.connected.publish({ connectParams: { @@ -546,8 +539,6 @@ function _resume (client, sync) { return } - const socket = client[kSocket] - if (client[kHTTPContext]) { client[kHTTPContext].resume() } @@ -580,27 +571,19 @@ function _resume (client, sync) { } client[kServerName] = request.servername - - if (socket && socket.servername !== request.servername) { - util.destroy(socket, new InformationalError('servername changed')) - return - } + client[kHTTPContext]?.destroy(new InformationalError('servername changed')) } if (client[kConnecting]) { return } - if (!socket) { + if (!client[kHTTPContext]) { connect(client) return } - if (socket.destroyed) { - return - } - - if (!client[kHTTPContext]) { + if (client[kHTTPContext].destroyed) { return } From ae870c1981dcb314b50e8f326c8b571841b4f58f Mon Sep 17 00:00:00 2001 From: Maksym Shenderuk Date: Wed, 28 Feb 2024 15:58:31 +0200 Subject: [PATCH 103/123] Request headers types (#2879) * Mention iterable in request headers type * Update type tests with iterable headers * Mention iterables in documentation --- docs/docs/api/Dispatcher.md | 42 ++++++++++++++++++++++++++++++--- test/types/dispatcher.test-d.ts | 11 +++++++++ types/dispatcher.d.ts | 2 +- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index 0c678fc8623..5b18be7a84c 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -855,10 +855,12 @@ Emitted when dispatcher is no longer busy. ## Parameter: `UndiciHeaders` -* `Record | string[] | null` - -Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. +* `Record | string[] | Iterable<[string, string | string[] | undefined]> | null` +Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in three forms: +* As an object specified by the `Record` (`IncomingHttpHeaders`) type. +* As an array of strings. An array representation of a header list must have an even length, or an `InvalidArgumentError` will be thrown. +* As an iterable that can encompass `Headers`, `Map`, or a custom iterator returning key-value pairs. Keys are lowercase and values are not modified. Response headers will derive a `host` from the `url` of the [Client](Client.md#class-client) instance if no `host` header was previously specified. @@ -886,3 +888,37 @@ Response headers will derive a `host` from the `url` of the [Client](Client.md#c 'accept', '*/*' ] ``` + +### Example 3 - Iterable + +```js +new Headers({ + 'content-length': '123', + 'content-type': 'text/plain', + connection: 'keep-alive', + host: 'mysite.com', + accept: '*/*' +}) +``` +or +```js +new Map([ + ['content-length', '123'], + ['content-type', 'text/plain'], + ['connection', 'keep-alive'], + ['host', 'mysite.com'], + ['accept', '*/*'] +]) +``` +or +```js +{ + *[Symbol.iterator] () { + yield ['content-length', '123'] + yield ['content-type', 'text/plain'] + yield ['connection', 'keep-alive'] + yield ['host', 'mysite.com'] + yield ['accept', '*/*'] + } +} +``` diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts index e40113bba5c..275d88c4af9 100644 --- a/test/types/dispatcher.test-d.ts +++ b/test/types/dispatcher.test-d.ts @@ -15,6 +15,14 @@ expectAssignable(new Dispatcher()) ['content-type']: 'application/json' } satisfies IncomingHttpHeaders; + const headerInstanceHeaders = new Headers({ hello: 'world' }) + const mapHeaders = new Map([['hello', 'world']]) + const iteratorHeaders = { + *[Symbol.iterator]() { + yield ['hello', 'world'] + } + } + // dispatch expectAssignable(dispatcher.dispatch({ path: '', method: 'GET' }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET' }, {})) @@ -23,6 +31,9 @@ expectAssignable(new Dispatcher()) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: {} }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: nodeCoreHeaders }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: null, reset: true }, {})) + expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: headerInstanceHeaders, reset: true }, {})) + expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: mapHeaders, reset: true }, {})) + expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: iteratorHeaders, reset: true }, {})) expectAssignable(dispatcher.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {})) // connect diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 0872df0fc0b..08c59ca0718 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -100,7 +100,7 @@ declare namespace Dispatcher { /** Default: `null` */ body?: string | Buffer | Uint8Array | Readable | null | FormData; /** Default: `null` */ - headers?: IncomingHttpHeaders | string[] | null; + headers?: IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null; /** Query string params to be embedded in the request URL. Default: `null` */ query?: Record; /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ From c90af4aa0d20e547c85da77e09cafae03493357e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 14:58:45 +0100 Subject: [PATCH 104/123] ci: remove concurrency (#2880) --- .github/workflows/nodejs.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 5baab2742c9..598220e662d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,9 +1,5 @@ name: Node CI -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - on: push: branches: From ed89b45ce7c7fbe0d46f66e32552e677ccbf6bfd Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 28 Feb 2024 17:36:42 +0100 Subject: [PATCH 105/123] fix: prefer queueMicrotask (#2881) --- lib/core/util.js | 4 ++-- lib/dispatcher/client.js | 4 ++-- lib/dispatcher/pool-base.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 9b331089b2a..9789a240575 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -204,9 +204,9 @@ function destroy (stream, err) { stream.destroy(err) } else if (err) { - process.nextTick((stream, err) => { + queueMicrotask(() => { stream.emit('error', err) - }, stream, err) + }) } if (stream.destroyed !== true) { diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 8cc334d0baf..d90ed6ad914 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -299,7 +299,7 @@ class Client extends DispatcherBase { } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { // Wait a tick in case stream/iterator is ended in the same tick. this[kResuming] = 1 - process.nextTick(resume, this) + queueMicrotask(() => resume(this)) } else { this[kResume](true) } @@ -548,7 +548,7 @@ function _resume (client, sync) { } else if (client[kNeedDrain] === 2) { if (sync) { client[kNeedDrain] = 1 - process.nextTick(emitDrain, client) + queueMicrotask(() => emitDrain(client)) } else { emitDrain(client) } diff --git a/lib/dispatcher/pool-base.js b/lib/dispatcher/pool-base.js index 93422f832e9..ff3108a4da2 100644 --- a/lib/dispatcher/pool-base.js +++ b/lib/dispatcher/pool-base.js @@ -158,7 +158,7 @@ class PoolBase extends DispatcherBase { this[kClients].push(client) if (this[kNeedDrain]) { - process.nextTick(() => { + queueMicrotask(() => { if (this[kNeedDrain]) { this[kOnDrain](client[kUrl], [this, client]) } From 3b2df8e6239997e672d891b9718e5bf9f27b8fa4 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Wed, 28 Feb 2024 18:07:24 +0100 Subject: [PATCH 106/123] chore: remove no-simd wasm of llhttp (#2871) --- .npmignore | 1 - build/wasm.js | 9 --------- lib/dispatcher/client-h1.js | 15 +-------------- lib/llhttp/llhttp-wasm.js | 3 --- lib/llhttp/llhttp.wasm | Bin 55466 -> 0 bytes 5 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 lib/llhttp/llhttp-wasm.js delete mode 100755 lib/llhttp/llhttp.wasm diff --git a/.npmignore b/.npmignore index 003eb6c62ff..0e8cd29ada1 100644 --- a/.npmignore +++ b/.npmignore @@ -5,7 +5,6 @@ # The wasm files are stored as base64 strings in the corresponding .js files lib/llhttp/llhttp_simd.wasm -lib/llhttp/llhttp.wasm !types/**/* !index.d.ts diff --git a/build/wasm.js b/build/wasm.js index ca89ec7d4cf..7f45839d076 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -79,15 +79,6 @@ module.exports = fs.readFileSync(require.resolve('./${basename(path)}')) `) } -// Build wasm binary -execSync(`${WASM_CC} ${WASM_CFLAGS} ${WASM_LDFLAGS} \ - ${join(WASM_SRC, 'src')}/*.c \ - -I${join(WASM_SRC, 'include')} \ - -o ${join(WASM_OUT, 'llhttp.wasm')} \ - ${WASM_LDLIBS}`, { stdio: 'inherit' }) - -writeWasmChunk('llhttp.wasm', 'llhttp-wasm.js') - // Build wasm simd binary execSync(`${WASM_CC} ${WASM_CFLAGS} -msimd128 ${WASM_LDFLAGS} \ ${join(WASM_SRC, 'src')}/*.c \ diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 62a3e29ef24..46b63087826 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -74,20 +74,7 @@ function removeAllListeners (obj) { } async function lazyllhttp () { - const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined - - let mod - try { - mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) - } catch (e) { - /* istanbul ignore next */ - - // We could check if the error was caused by the simd option not - // being enabled, but the occurring of this other error - // * https://github.com/emscripten-core/emscripten/issues/11495 - // got me to remove that check to avoid breaking Node 12. - mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) - } + const mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) return await WebAssembly.instantiate(mod, { env: { diff --git a/lib/llhttp/llhttp-wasm.js b/lib/llhttp/llhttp-wasm.js deleted file mode 100644 index 0c0311383ab..00000000000 --- a/lib/llhttp/llhttp-wasm.js +++ /dev/null @@ -1,3 +0,0 @@ -const { Buffer } = require('node:buffer') - -module.exports = Buffer.from('AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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ADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkB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QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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FCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAU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YWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=', 'base64') diff --git a/lib/llhttp/llhttp.wasm b/lib/llhttp/llhttp.wasm deleted file mode 100755 index fe63282c9a5e6f49066a0c6fc7a2dd7293204c33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55466 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 From 6cd5f09acbebd3f4408beb76d0b95c5be8fabc0a Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 29 Feb 2024 09:25:27 +0100 Subject: [PATCH 107/123] cookies: improve validateCookieValue (#2883) * cookies: improve validateCookieValue * Apply suggestions from code review --- benchmarks/cookies/validate-cookie-value.mjs | 16 ++++ lib/web/cookies/util.js | 29 ++++++-- test/cookie/cookies.js | 4 +- test/cookie/validate-cookie-value.js | 78 ++++++++++++++++++++ 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 benchmarks/cookies/validate-cookie-value.mjs create mode 100644 test/cookie/validate-cookie-value.js diff --git a/benchmarks/cookies/validate-cookie-value.mjs b/benchmarks/cookies/validate-cookie-value.mjs new file mode 100644 index 00000000000..a10db83b4c1 --- /dev/null +++ b/benchmarks/cookies/validate-cookie-value.mjs @@ -0,0 +1,16 @@ +import { bench, group, run } from 'mitata' +import { validateCookieValue } from '../../lib/web/cookies/util.js' + +const valid = 'Cat' +const wrappedValid = `"${valid}"` + +group('validateCookieValue', () => { + bench(`valid: ${valid}`, () => { + return validateCookieValue(valid) + }) + bench(`valid: ${wrappedValid}`, () => { + return validateCookieValue(wrappedValid) + }) +}) + +await run() diff --git a/lib/web/cookies/util.js b/lib/web/cookies/util.js index b043054d1ce..51ed2b5434c 100644 --- a/lib/web/cookies/util.js +++ b/lib/web/cookies/util.js @@ -69,18 +69,30 @@ function validateCookieName (name) { * @param {string} value */ function validateCookieValue (value) { - for (const char of value) { - const code = char.charCodeAt(0) + let len = value.length + let i = 0 + + // if the value is wrapped in DQUOTE + if (value[0] === '"') { + if (len === 1 || value[len - 1] !== '"') { + throw new Error('Invalid cookie value') + } + --len + ++i + } + + while (i < len) { + const code = value.charCodeAt(i++) if ( code < 0x21 || // exclude CTLs (0-31) - code === 0x22 || - code === 0x2C || - code === 0x3B || - code === 0x5C || - code > 0x7E // non-ascii + code > 0x7E || // non-ascii and DEL (127) + code === 0x22 || // " + code === 0x2C || // , + code === 0x3B || // ; + code === 0x5C // \ ) { - throw new Error('Invalid header value') + throw new Error('Invalid cookie value') } } } @@ -286,6 +298,7 @@ function getHeadersList (headers) { module.exports = { isCTLExcludingHtab, validateCookiePath, + validateCookieValue, toIMFDate, stringify, getHeadersList diff --git a/test/cookie/cookies.js b/test/cookie/cookies.js index 4185559466b..711f26a0351 100644 --- a/test/cookie/cookies.js +++ b/test/cookie/cookies.js @@ -116,7 +116,7 @@ test('Cookie Value Validation', () => { } ) }, - new Error('Invalid header value'), + new Error('Invalid cookie value'), "RFC2616 cookie 'Space'" ) }) @@ -128,7 +128,7 @@ test('Cookie Value Validation', () => { value: 'United Kingdom' }) }, - new Error('Invalid header value'), + new Error('Invalid cookie value'), "RFC2616 cookie 'location' cannot contain character ' '" ) }) diff --git a/test/cookie/validate-cookie-value.js b/test/cookie/validate-cookie-value.js new file mode 100644 index 00000000000..7511121fb08 --- /dev/null +++ b/test/cookie/validate-cookie-value.js @@ -0,0 +1,78 @@ +'use strict' + +const { test, describe } = require('node:test') +const { throws, strictEqual } = require('node:assert') + +const { + validateCookieValue +} = require('../../lib/web/cookies/util') + +describe('validateCookieValue', () => { + test('should throw for CTLs', () => { + throws(() => validateCookieValue('\x00'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x01'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x02'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x03'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x04'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x05'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x06'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x07'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x08'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x09'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0A'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0B'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0C'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0D'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0E'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x0F'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x10'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x11'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x12'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x13'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x14'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x15'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x16'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x17'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x18'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x19'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1A'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1B'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1C'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1D'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1E'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x1F'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('\x7F'), new Error('Invalid cookie value')) + }) + + test('should throw for ; character', () => { + throws(() => validateCookieValue(';'), new Error('Invalid cookie value')) + }) + + test('should throw for " character', () => { + throws(() => validateCookieValue('"'), new Error('Invalid cookie value')) + }) + + test('should throw for , character', () => { + throws(() => validateCookieValue(','), new Error('Invalid cookie value')) + }) + + test('should throw for \\ character', () => { + throws(() => validateCookieValue('\\'), new Error('Invalid cookie value')) + }) + + test('should pass for a printable character', t => { + strictEqual(validateCookieValue('A'), undefined) + strictEqual(validateCookieValue('Z'), undefined) + strictEqual(validateCookieValue('a'), undefined) + strictEqual(validateCookieValue('z'), undefined) + strictEqual(validateCookieValue('!'), undefined) + strictEqual(validateCookieValue('='), undefined) + }) + + test('should handle strings wrapped in DQUOTE', t => { + strictEqual(validateCookieValue('""'), undefined) + strictEqual(validateCookieValue('"helloworld"'), undefined) + throws(() => validateCookieValue('"'), new Error('Invalid cookie value')) + throws(() => validateCookieValue('"""'), new Error('Invalid cookie value')) + }) +}) From 41068309078f724a7fb4ad18eef097105ed343e1 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 29 Feb 2024 09:25:34 +0100 Subject: [PATCH 108/123] cookies: improve validateCookieName (#2884) --- benchmarks/cookies/validate-cookie-name.mjs | 12 ++ lib/web/cookies/util.js | 42 ++++--- test/cookie/validate-cookie-name.js | 130 ++++++++++++++++++++ 3 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 benchmarks/cookies/validate-cookie-name.mjs create mode 100644 test/cookie/validate-cookie-name.js diff --git a/benchmarks/cookies/validate-cookie-name.mjs b/benchmarks/cookies/validate-cookie-name.mjs new file mode 100644 index 00000000000..70e2b8a7406 --- /dev/null +++ b/benchmarks/cookies/validate-cookie-name.mjs @@ -0,0 +1,12 @@ +import { bench, group, run } from 'mitata' +import { validateCookieName } from '../../lib/web/cookies/util.js' + +const valid = 'Cat' + +group('validateCookieName', () => { + bench(`valid: ${valid}`, () => { + return validateCookieName(valid) + }) +}) + +await run() diff --git a/lib/web/cookies/util.js b/lib/web/cookies/util.js index 51ed2b5434c..d49857175a4 100644 --- a/lib/web/cookies/util.js +++ b/lib/web/cookies/util.js @@ -32,28 +32,29 @@ function isCTLExcludingHtab (value) { * @param {string} name */ function validateCookieName (name) { - for (const char of name) { - const code = char.charCodeAt(0) + for (let i = 0; i < name.length; ++i) { + const code = name.charCodeAt(i) if ( - (code <= 0x20 || code > 0x7F) || - char === '(' || - char === ')' || - char === '>' || - char === '<' || - char === '@' || - char === ',' || - char === ';' || - char === ':' || - char === '\\' || - char === '"' || - char === '/' || - char === '[' || - char === ']' || - char === '?' || - char === '=' || - char === '{' || - char === '}' + code < 0x21 || // exclude CTLs (0-31), SP and HT + code > 0x7E || // exclude non-ascii and DEL + code === 0x22 || // " + code === 0x28 || // ( + code === 0x29 || // ) + code === 0x3C || // < + code === 0x3E || // > + code === 0x40 || // @ + code === 0x2C || // , + code === 0x3B || // ; + code === 0x3A || // : + code === 0x5C || // \ + code === 0x2F || // / + code === 0x5B || // [ + code === 0x5D || // ] + code === 0x3F || // ? + code === 0x3D || // = + code === 0x7B || // { + code === 0x7D // } ) { throw new Error('Invalid cookie name') } @@ -297,6 +298,7 @@ function getHeadersList (headers) { module.exports = { isCTLExcludingHtab, + validateCookieName, validateCookiePath, validateCookieValue, toIMFDate, diff --git a/test/cookie/validate-cookie-name.js b/test/cookie/validate-cookie-name.js new file mode 100644 index 00000000000..32e4d9d2b66 --- /dev/null +++ b/test/cookie/validate-cookie-name.js @@ -0,0 +1,130 @@ +'use strict' + +const { test, describe } = require('node:test') +const { throws, strictEqual } = require('node:assert') + +const { + validateCookieName +} = require('../../lib/web/cookies/util') + +describe('validateCookieName', () => { + test('should throw for CTLs', () => { + throws(() => validateCookieName('\x00'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x01'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x02'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x03'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x04'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x05'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x06'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x07'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x08'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x09'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0A'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0B'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0C'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0D'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0E'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x0F'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x10'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x11'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x12'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x13'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x14'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x15'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x16'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x17'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x18'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x19'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1A'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1B'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1C'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1D'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1E'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x1F'), new Error('Invalid cookie name')) + throws(() => validateCookieName('\x7F'), new Error('Invalid cookie name')) + }) + + test('should throw for " " character', () => { + throws(() => validateCookieName(' '), new Error('Invalid cookie name')) + }) + + test('should throw for Horizontal Tab character', () => { + throws(() => validateCookieName('\t'), new Error('Invalid cookie name')) + }) + + test('should throw for ; character', () => { + throws(() => validateCookieName(';'), new Error('Invalid cookie name')) + }) + + test('should throw for " character', () => { + throws(() => validateCookieName('"'), new Error('Invalid cookie name')) + }) + + test('should throw for , character', () => { + throws(() => validateCookieName(','), new Error('Invalid cookie name')) + }) + + test('should throw for \\ character', () => { + throws(() => validateCookieName('\\'), new Error('Invalid cookie name')) + }) + + test('should throw for ( character', () => { + throws(() => validateCookieName('('), new Error('Invalid cookie name')) + }) + + test('should throw for ) character', () => { + throws(() => validateCookieName(')'), new Error('Invalid cookie name')) + }) + + test('should throw for < character', () => { + throws(() => validateCookieName('<'), new Error('Invalid cookie name')) + }) + + test('should throw for > character', () => { + throws(() => validateCookieName('>'), new Error('Invalid cookie name')) + }) + + test('should throw for @ character', () => { + throws(() => validateCookieName('@'), new Error('Invalid cookie name')) + }) + + test('should throw for : character', () => { + throws(() => validateCookieName(':'), new Error('Invalid cookie name')) + }) + + test('should throw for / character', () => { + throws(() => validateCookieName('/'), new Error('Invalid cookie name')) + }) + + test('should throw for [ character', () => { + throws(() => validateCookieName('['), new Error('Invalid cookie name')) + }) + + test('should throw for ] character', () => { + throws(() => validateCookieName(']'), new Error('Invalid cookie name')) + }) + + test('should throw for ? character', () => { + throws(() => validateCookieName('?'), new Error('Invalid cookie name')) + }) + + test('should throw for = character', () => { + throws(() => validateCookieName('='), new Error('Invalid cookie name')) + }) + + test('should throw for { character', () => { + throws(() => validateCookieName('{'), new Error('Invalid cookie name')) + }) + + test('should throw for } character', () => { + throws(() => validateCookieName('}'), new Error('Invalid cookie name')) + }) + + test('should pass for a printable character', t => { + strictEqual(validateCookieName('A'), undefined) + strictEqual(validateCookieName('Z'), undefined) + strictEqual(validateCookieName('a'), undefined) + strictEqual(validateCookieName('z'), undefined) + strictEqual(validateCookieName('!'), undefined) + }) +}) From a3f494bf5b88d6856297a852ce9b2d476fd3594b Mon Sep 17 00:00:00 2001 From: Jean Pierre Date: Thu, 29 Feb 2024 03:59:19 -0500 Subject: [PATCH 109/123] Properly parse set-cookie header using http2 (#2886) * Properly parse set-cookie header using http2 * :lipstick: Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --------- Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- lib/web/fetch/index.js | 10 +++++++++- test/fetch/cookies.js | 45 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 0ae6a704e11..37e269fbc93 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -2145,7 +2145,15 @@ async function httpNetworkFetch ( const keys = Object.keys(rawHeaders) for (let i = 0; i < keys.length; ++i) { // The header names are already in lowercase. - headersList.append(keys[i], rawHeaders[keys[i]], true) + const key = keys[i] + const value = rawHeaders[key] + if (key === 'set-cookie') { + for (let j = 0; j < value.length; ++j) { + headersList.append(key, value[j], true) + } + } else { + headersList.append(key, value, true) + } } // For H2, The header names are already in lowercase, // so we can avoid the `HeadersList#get` call here. diff --git a/test/fetch/cookies.js b/test/fetch/cookies.js index c34f3dc921a..3bc69c6837b 100644 --- a/test/fetch/cookies.js +++ b/test/fetch/cookies.js @@ -5,8 +5,11 @@ const { createServer } = require('node:http') const { test } = require('node:test') const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') -const { fetch, Headers } = require('../..') +const { Client, fetch, Headers } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') +const pem = require('https-pem') +const { createSecureServer } = require('node:http2') +const { closeClientAndServerAsPromise } = require('../utils/node-http') test('Can receive set-cookie headers from a server using fetch - issue #1262', async (t) => { const server = createServer((req, res) => { @@ -66,3 +69,43 @@ test('Cookie header is delimited with a semicolon rather than a comma - issue #1 ] }) }) + +test('Can receive set-cookie headers from a http2 server using fetch - issue #2885', async (t) => { + const server = createSecureServer(pem) + server.on('stream', async (stream, headers) => { + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-method': headers[':method'], + 'set-cookie': 'Space=Cat; Secure; HttpOnly', + ':status': 200 + }) + + stream.end('test') + }) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'GET', + dispatcher: client, + headers: { + 'content-type': 'text-plain' + } + } + ) + + t.after(closeClientAndServerAsPromise(client, server)) + + assert.deepStrictEqual(response.headers.getSetCookie(), ['Space=Cat; Secure; HttpOnly']) +}) From e343948ec3a8b1052f50f80b109692dfd545f4eb Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 1 Mar 2024 03:43:14 -0500 Subject: [PATCH 110/123] doc deprecate bodymixin.formData (#2892) * doc deprecate bodymixin.formData * fixup * add dedicated body mixin section --- docs/README.md | 8 ++++++-- docs/docs/api/Fetch.md | 32 +++++++++++++++++++++++++++++++- types/fetch.d.ts | 41 ++++++++++++++++++++--------------------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/docs/README.md b/docs/README.md index 144b4cb6534..84655d48cb0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,10 +60,14 @@ console.log('trailers', trailers) The `body` mixins are the most common way to format the request/response body. Mixins include: -- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) +- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) +- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob) - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) +> [!NOTE] +> The body returned from `undici.request` does not implement `.formData()`. + Example usage: ```js @@ -226,7 +230,7 @@ await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' - half -In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex) +In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`, however, fetch requests are currently always full duplex. For more detail refer to the [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex). #### `response.body` diff --git a/docs/docs/api/Fetch.md b/docs/docs/api/Fetch.md index b5a62422a24..5e480f5acfa 100644 --- a/docs/docs/api/Fetch.md +++ b/docs/docs/api/Fetch.md @@ -12,7 +12,9 @@ In Node versions v18.13.0 and above and v19.2.0 and above, undici will default t ## FormData -This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData) +This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData). + +If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored. ## Response @@ -25,3 +27,31 @@ This API is implemented as per the standard, you can find documentation on [MDN] ## Header This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers) + +# Body Mixins + +`Response` and `Request` body inherit body mixin methods. These methods include: + +- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) +- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob) +- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) +- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) +- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) + +There is an ongoing discussion regarding `.formData()` and its usefulness and performance in server environments. It is recommended to use a dedicated library for parsing `multipart/form-data` bodies, such as [Busboy](https://www.npmjs.com/package/busboy) or [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy). + +These libraries can be interfaced with fetch with the following example code: + +```mjs +import { Busboy } from '@fastify/busboy' +import { Readable } from 'node:stream' + +const response = await fetch('...') +const busboy = new Busboy({ + headers: { + 'content-type': response.headers.get('content-type') + } +}) + +Readable.fromWeb(response.body).pipe(busboy) +``` diff --git a/types/fetch.d.ts b/types/fetch.d.ts index 440f2b00397..68779da13ba 100644 --- a/types/fetch.d.ts +++ b/types/fetch.d.ts @@ -27,12 +27,29 @@ export type BodyInit = | null | string -export interface BodyMixin { +export class BodyMixin { readonly body: ReadableStream | null readonly bodyUsed: boolean readonly arrayBuffer: () => Promise readonly blob: () => Promise + /** + * @deprecated This method is not recommended for parsing multipart/form-data bodies in server environments. + * It is recommended to use a library such as [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy) as follows: + * + * @example + * ```js + * import { Busboy } from '@fastify/busboy' + * import { Readable } from 'node:stream' + * + * const response = await fetch('...') + * const busboy = new Busboy({ headers: { 'content-type': response.headers.get('content-type') } }) + * + * // handle events emitted from `busboy` + * + * Readable.fromWeb(response.body).pipe(busboy) + * ``` + */ readonly formData: () => Promise readonly json: () => Promise readonly text: () => Promise @@ -135,7 +152,7 @@ export type RequestRedirect = 'error' | 'follow' | 'manual' export type RequestDuplex = 'half' -export declare class Request implements BodyMixin { +export declare class Request extends BodyMixin { constructor (input: RequestInfo, init?: RequestInit) readonly cache: RequestCache @@ -153,15 +170,6 @@ export declare class Request implements BodyMixin { readonly signal: AbortSignal readonly duplex: RequestDuplex - readonly body: ReadableStream | null - readonly bodyUsed: boolean - - readonly arrayBuffer: () => Promise - readonly blob: () => Promise - readonly formData: () => Promise - readonly json: () => Promise - readonly text: () => Promise - readonly clone: () => Request } @@ -181,7 +189,7 @@ export type ResponseType = export type ResponseRedirectStatus = 301 | 302 | 303 | 307 | 308 -export declare class Response implements BodyMixin { +export declare class Response extends BodyMixin { constructor (body?: BodyInit, init?: ResponseInit) readonly headers: Headers @@ -192,15 +200,6 @@ export declare class Response implements BodyMixin { readonly url: string readonly redirected: boolean - readonly body: ReadableStream | null - readonly bodyUsed: boolean - - readonly arrayBuffer: () => Promise - readonly blob: () => Promise - readonly formData: () => Promise - readonly json: () => Promise - readonly text: () => Promise - readonly clone: () => Response static error (): Response From ef5fec820ed3f08b49ff4e1303d7900f1a9a1d08 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 1 Mar 2024 22:36:26 +0900 Subject: [PATCH 111/123] perf: optimize check invalid field-vchar (#2889) * perf: optimize check invalid field-vchar * add benchmark, use named import, fix regression * fix benchmark * apply suggestions from code review --- benchmarks/core/is-valid-header-char.mjs | 57 ++++++++++++++++++++++++ lib/core/request.js | 56 +++++++++++------------ lib/core/util.js | 20 ++++++++- 3 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 benchmarks/core/is-valid-header-char.mjs diff --git a/benchmarks/core/is-valid-header-char.mjs b/benchmarks/core/is-valid-header-char.mjs new file mode 100644 index 00000000000..4734d119011 --- /dev/null +++ b/benchmarks/core/is-valid-header-char.mjs @@ -0,0 +1,57 @@ +import { bench, group, run } from 'mitata' +import { isValidHeaderChar } from '../../lib/core/util.js' + +const html = 'text/html' +const json = 'application/json; charset=UTF-8' + +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +/** + * @param {string} characters + */ +function charCodeAtApproach (characters) { + // Validate if characters is a valid field-vchar. + // field-value = *( field-content / obs-fold ) + // field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + // field-vchar = VCHAR / obs-text + for (let i = 0; i < characters.length; ++i) { + const code = characters.charCodeAt(i) + // not \x20-\x7e, \t and \x80-\xff + if ((code < 0x20 && code !== 0x09) || code === 0x7f || code > 0xff) { + return false + } + } + return true +} + +group(`isValidHeaderChar# ${html}`, () => { + bench('regexp.test', () => { + return !headerCharRegex.test(html) + }) + bench('regexp.exec', () => { + return headerCharRegex.exec(html) === null + }) + bench('charCodeAt', () => { + return charCodeAtApproach(html) + }) + bench('isValidHeaderChar', () => { + return isValidHeaderChar(html) + }) +}) + +group(`isValidHeaderChar# ${json}`, () => { + bench('regexp.test', () => { + return !headerCharRegex.test(json) + }) + bench('regexp.exec', () => { + return headerCharRegex.exec(json) === null + }) + bench('charCodeAt', () => { + return charCodeAtApproach(json) + }) + bench('isValidHeaderChar', () => { + return isValidHeaderChar(json) + }) +}) + +await run() diff --git a/lib/core/request.js b/lib/core/request.js index dc136408b55..45f349c85c6 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -5,21 +5,22 @@ const { NotSupportedError } = require('./errors') const assert = require('node:assert') -const util = require('./util') +const { + isValidHTTPToken, + isValidHeaderChar, + isStream, + destroy, + isBuffer, + isFormDataLike, + isIterable, + isBlobLike, + buildURL, + validateHandler, + getServerName +} = require('./util') const { channels } = require('./diagnostics.js') const { headerNameLowerCasedRecord } = require('./constants') -// headerCharRegex have been lifted from -// https://github.com/nodejs/node/blob/main/lib/_http_common.js - -/** - * Matches if val contains an invalid field-vchar - * field-value = *( field-content / obs-fold ) - * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] - * field-vchar = VCHAR / obs-text - */ -const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ - // Verifies that a given path is valid does not contain control chars \x00 to \x20 const invalidPathRegex = /[^\u0021-\u00ff]/ @@ -55,7 +56,7 @@ class Request { if (typeof method !== 'string') { throw new InvalidArgumentError('method must be a string') - } else if (!util.isValidHTTPToken(method)) { + } else if (!isValidHTTPToken(method)) { throw new InvalidArgumentError('invalid request method') } @@ -91,13 +92,13 @@ class Request { if (body == null) { this.body = null - } else if (util.isStream(body)) { + } else if (isStream(body)) { this.body = body const rState = this.body._readableState if (!rState || !rState.autoDestroy) { this.endHandler = function autoDestroy () { - util.destroy(this) + destroy(this) } this.body.on('end', this.endHandler) } @@ -110,7 +111,7 @@ class Request { } } this.body.on('error', this.errorHandler) - } else if (util.isBuffer(body)) { + } else if (isBuffer(body)) { this.body = body.byteLength ? body : null } else if (ArrayBuffer.isView(body)) { this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null @@ -118,7 +119,7 @@ class Request { this.body = body.byteLength ? Buffer.from(body) : null } else if (typeof body === 'string') { this.body = body.length ? Buffer.from(body) : null - } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) { this.body = body } else { throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') @@ -130,7 +131,7 @@ class Request { this.upgrade = upgrade || null - this.path = query ? util.buildURL(path, query) : path + this.path = query ? buildURL(path, query) : path this.origin = origin @@ -166,22 +167,21 @@ class Request { if (!Array.isArray(header) || header.length !== 2) { throw new InvalidArgumentError('headers must be in key-value pair format') } - const [key, value] = header - processHeader(this, key, value) + processHeader(this, header[0], header[1]) } } else { const keys = Object.keys(headers) - for (const key of keys) { - processHeader(this, key, headers[key]) + for (let i = 0; i < keys.length; ++i) { + processHeader(this, keys[i], headers[keys[i]]) } } } else if (headers != null) { throw new InvalidArgumentError('headers must be an object or an array') } - util.validateHandler(handler, method, upgrade) + validateHandler(handler, method, upgrade) - this.servername = util.getServerName(this.host) + this.servername = getServerName(this.host) this[kHandler] = handler @@ -315,7 +315,7 @@ class Request { } } -function processHeader (request, key, val, skipAppend = false) { +function processHeader (request, key, val) { if (val && (typeof val === 'object' && !Array.isArray(val))) { throw new InvalidArgumentError(`invalid ${key} header`) } else if (val === undefined) { @@ -326,7 +326,7 @@ function processHeader (request, key, val, skipAppend = false) { if (headerName === undefined) { headerName = key.toLowerCase() - if (headerNameLowerCasedRecord[headerName] === undefined && !util.isValidHTTPToken(headerName)) { + if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) { throw new InvalidArgumentError('invalid header key') } } @@ -335,7 +335,7 @@ function processHeader (request, key, val, skipAppend = false) { const arr = [] for (let i = 0; i < val.length; i++) { if (typeof val[i] === 'string') { - if (headerCharRegex.exec(val[i]) !== null) { + if (!isValidHeaderChar(val[i])) { throw new InvalidArgumentError(`invalid ${key} header`) } arr.push(val[i]) @@ -349,7 +349,7 @@ function processHeader (request, key, val, skipAppend = false) { } val = arr } else if (typeof val === 'string') { - if (headerCharRegex.exec(val) !== null) { + if (!isValidHeaderChar(val)) { throw new InvalidArgumentError(`invalid ${key} header`) } } else if (val === null) { diff --git a/lib/core/util.js b/lib/core/util.js index 9789a240575..cbb6d7495e5 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -495,6 +495,24 @@ function isValidHTTPToken (characters) { return true } +// headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +/** + * @param {string} characters + */ +function isValidHeaderChar (characters) { + return !headerCharRegex.test(characters) +} + // Parsed accordingly to RFC 9110 // https://www.rfc-editor.org/rfc/rfc9110#field.content-range function parseRangeHeader (range) { @@ -516,7 +534,6 @@ kEnumerableProperty.enumerable = true module.exports = { kEnumerableProperty, nop, - isDisturbed, isErrored, isReadable, @@ -546,6 +563,7 @@ module.exports = { buildURL, addAbortListener, isValidHTTPToken, + isValidHeaderChar, isTokenCharCode, parseRangeHeader, nodeMajor, From dde9dcb6a219c7c1b34e43b872f5f4e68dc04a02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:02:09 +0000 Subject: [PATCH 112/123] build(deps): bump github/codeql-action from 3.24.5 to 3.24.6 (#2897) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.5 to 3.24.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/47b3d888fe66b639e431abf22ebca059152f1eea...8a470fddafa5cbb6266ee11b37ef4d8aae19c571) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6c2848d72e5..47f83e6c296 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 + uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 + uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v2.3.3 + uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bbb98bf925e..8289262a937 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: sarif_file: results.sarif From 28b2df8b718d91647f47c03ea30de8e057c4ab67 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 1 Mar 2024 23:57:51 -0500 Subject: [PATCH 113/123] fix issue 2898 (#2900) * fix issue 2898 --- lib/web/fetch/body.js | 6 +----- lib/web/fetch/index.js | 6 +++--- lib/web/fetch/request.js | 2 +- test/fetch/issue-2898.js | 33 +++++++++++++++++++++++++++++++ test/wpt/status/fetch.status.json | 6 ++---- 5 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 test/fetch/issue-2898.js diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js index 932df3e6532..12377cb511d 100644 --- a/lib/web/fetch/body.js +++ b/lib/web/fetch/body.js @@ -275,17 +275,13 @@ function cloneBody (body) { // 1. Let « out1, out2 » be the result of teeing body’s stream. const [out1, out2] = body.stream.tee() - const out2Clone = structuredClone(out2, { transfer: [out2] }) - // This, for whatever reasons, unrefs out2Clone which allows - // the process to exit by itself. - const [, finalClone] = out2Clone.tee() // 2. Set body’s stream to out1. body.stream = out1 // 3. Return a body whose stream is out2 and other members are copied from body. return { - stream: finalClone, + stream: out2, length: body.length, source: body.source } diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 37e269fbc93..d8c20c59bf7 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -10,7 +10,7 @@ const { fromInnerResponse } = require('./response') const { HeadersList } = require('./headers') -const { Request, makeRequest } = require('./request') +const { Request, cloneRequest } = require('./request') const zlib = require('node:zlib') const { bytesMatch, @@ -1405,7 +1405,7 @@ async function httpNetworkOrCacheFetch ( // Otherwise: // 1. Set httpRequest to a clone of request. - httpRequest = makeRequest(request) + httpRequest = cloneRequest(request) // 2. Set httpFetchParams to a copy of fetchParams. httpFetchParams = { ...fetchParams } @@ -1942,7 +1942,7 @@ async function httpNetworkFetch ( // 17. Run these steps, but abort when the ongoing fetch is terminated: // 1. Set response’s body to a new body whose stream is stream. - response.body = { stream } + response.body = { stream, source: null, length: null } // 2. If response is not a network error and request’s cache mode is // not "no-store", then update response in httpCache for request. diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index afe92499267..be89ed0d8d7 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -990,4 +990,4 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ } ]) -module.exports = { Request, makeRequest, fromInnerRequest } +module.exports = { Request, makeRequest, fromInnerRequest, cloneRequest } diff --git a/test/fetch/issue-2898.js b/test/fetch/issue-2898.js new file mode 100644 index 00000000000..231b7615761 --- /dev/null +++ b/test/fetch/issue-2898.js @@ -0,0 +1,33 @@ +'use strict' + +const assert = require('node:assert') +const { once } = require('node:events') +const { createServer } = require('node:http') +const { test } = require('node:test') +const { fetch } = require('../..') + +// https://github.com/nodejs/undici/issues/2898 +test('421 requests with a body work as expected', async (t) => { + const expected = 'This is a 421 Misdirected Request response.' + + const server = createServer((req, res) => { + res.statusCode = 421 + res.end(expected) + }).listen(0) + + t.after(server.close.bind(server)) + await once(server, 'listening') + + for (const body of [ + 'hello', + new Uint8Array(Buffer.from('helloworld', 'utf-8')) + ]) { + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + body + }) + + assert.deepStrictEqual(response.status, 421) + assert.deepStrictEqual(await response.text(), expected) + } +}) diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 3b2bfa002f0..008baf5bd12 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -391,10 +391,8 @@ }, "response": { "response-clone.any.js": { - "fail": [ - "Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)", - "Check response clone use structureClone for teed ReadableStreams (DataViewchunk)" - ] + "note": "Node streams are too buggy currently.", + "skip": true }, "response-consume-empty.any.js": { "fail": [ From 7a92eeeddf14ea637f5a6ab39c20bf18ab0e7ce0 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 2 Mar 2024 13:27:29 +0100 Subject: [PATCH 114/123] tests: ignore catch block when requiring crypto module (#2901) --- lib/web/fetch/util.js | 4 ++-- lib/web/websocket/connection.js | 1 + lib/web/websocket/frame.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index 92bcb6cb202..6cf679b2ced 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -12,11 +12,11 @@ const { isUint8Array } = require('node:util/types') const { webidl } = require('./webidl') // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable -/** @type {import('crypto')|undefined} */ +/** @type {import('crypto')} */ let crypto - try { crypto = require('node:crypto') +/* c8 ignore next 3 */ } catch { } diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 45f68e1de93..2869ebafb63 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -20,6 +20,7 @@ const { kHeadersList } = require('../../core/symbols') let crypto try { crypto = require('node:crypto') +/* c8 ignore next 3 */ } catch { } diff --git a/lib/web/websocket/frame.js b/lib/web/websocket/frame.js index ee3087bdf70..30c68c811cd 100644 --- a/lib/web/websocket/frame.js +++ b/lib/web/websocket/frame.js @@ -6,6 +6,7 @@ const { maxUnsigned16Bit } = require('./constants') let crypto try { crypto = require('node:crypto') +/* c8 ignore next 3 */ } catch { } From fc4e766e97c2d3571e845d017784df99d8e450b6 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sat, 2 Mar 2024 16:12:56 +0100 Subject: [PATCH 115/123] websocket: remove dead code in parseCloseBody (#2902) --- lib/web/websocket/receiver.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 18dc474f16e..eff9aea569f 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -102,7 +102,7 @@ class ByteParser extends Writable { const body = this.consume(payloadLength) - this.#info.closeInfo = this.parseCloseBody(false, body) + this.#info.closeInfo = this.parseCloseBody(body) if (!this.ws[kSentClose]) { // If an endpoint receives a Close frame and did not previously send a @@ -290,7 +290,7 @@ class ByteParser extends Writable { return buffer } - parseCloseBody (onlyCode, data) { + parseCloseBody (data) { // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 /** @type {number|undefined} */ let code @@ -302,14 +302,6 @@ class ByteParser extends Writable { code = data.readUInt16BE(0) } - if (onlyCode) { - if (!isValidStatusCode(code)) { - return null - } - - return { code } - } - // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 /** @type {Buffer} */ let reason = data.subarray(2) From 1dbb0aa25c5a09820e429ef4c1f5bdad44e7fb70 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 3 Mar 2024 06:14:57 +0100 Subject: [PATCH 116/123] fix: tests dont need process.exit (#2909) --- test/fetch/client-fetch.js | 4 +--- test/node-test/agent.js | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index e0fcb97eca1..e258f9ab6d4 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -2,7 +2,7 @@ 'use strict' -const { test, after } = require('node:test') +const { test } = require('node:test') const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') @@ -700,5 +700,3 @@ test('Receiving non-Latin1 headers', async (t) => { assert.deepStrictEqual(cdHeaders, ContentDisposition) assert.deepStrictEqual(lengths, [30, 34, 94, 104, 90]) }) - -after(() => process.exit()) diff --git a/test/node-test/agent.js b/test/node-test/agent.js index c077fbab4e8..91dd99e9bf7 100644 --- a/test/node-test/agent.js +++ b/test/node-test/agent.js @@ -808,5 +808,3 @@ test('the dispatcher is truly global', t => { const undiciFresh = importFresh('../../index.js') assert.strictEqual(agent, undiciFresh.getGlobalDispatcher()) }) - -after(() => process.exit()) From 3608e61b4b8f6b89bf27a012f4023970b911b93e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 3 Mar 2024 08:18:42 +0100 Subject: [PATCH 117/123] chore: remove proxyquire (#2906) --- package.json | 1 - test/pool.js | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index bc1d2f6520c..d71bac7a179 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "jsfuzz": "^1.0.15", "pre-commit": "^1.2.2", "proxy": "^2.1.1", - "proxyquire": "^2.1.3", "snazzy": "^9.0.0", "standard": "^17.0.0", "tsd": "^0.30.1", diff --git a/test/pool.js b/test/pool.js index 5e84f4402bb..b75cd530d43 100644 --- a/test/pool.js +++ b/test/pool.js @@ -11,7 +11,6 @@ const { Readable } = require('node:stream') const { promisify } = require('node:util') -const proxyquire = require('proxyquire') const { kBusy, kPending, @@ -366,17 +365,15 @@ test('backpressure algorithm', async (t) => { } } - const Pool = proxyquire('../lib/dispatcher/pool', { - './client': FakeClient - }) - const noopHandler = { onError (err) { throw err } } - const pool = new Pool('http://notahost') + const pool = new Pool('http://notahost', { + factory: () => new FakeClient() + }) pool.dispatch({}, noopHandler) pool.dispatch({}, noopHandler) From b4ebda2fff47878431e6c27656088f1f13989b72 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 3 Mar 2024 09:18:51 +0100 Subject: [PATCH 118/123] chore: remove import-fresh as devDependency (#2908) --- package.json | 1 - test/node-test/agent.js | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d71bac7a179..8fdfd7a4bc4 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,6 @@ "formdata-node": "^6.0.3", "https-pem": "^3.0.0", "husky": "^9.0.7", - "import-fresh": "^3.3.0", "jest": "^29.0.2", "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", diff --git a/test/node-test/agent.js b/test/node-test/agent.js index 91dd99e9bf7..74a3ae26e77 100644 --- a/test/node-test/agent.js +++ b/test/node-test/agent.js @@ -15,7 +15,6 @@ const { setGlobalDispatcher, getGlobalDispatcher } = require('../..') -const importFresh = require('import-fresh') const { tspl } = require('@matteo.collina/tspl') const { closeServerAsPromise } = require('../utils/node-http') @@ -805,6 +804,10 @@ test('connect is not valid', t => { test('the dispatcher is truly global', t => { const agent = getGlobalDispatcher() - const undiciFresh = importFresh('../../index.js') + assert.ok(require.resolve('../../index.js') in require.cache) + delete require.cache[require.resolve('../../index.js')] + assert.strictEqual(require.resolve('../../index.js') in require.cache, false) + const undiciFresh = require('../../index.js') + assert.ok(require.resolve('../../index.js') in require.cache) assert.strictEqual(agent, undiciFresh.getGlobalDispatcher()) }) From 1216ba04a145028bc6839e282cc4a2c5dfae3845 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 3 Mar 2024 18:14:24 +0900 Subject: [PATCH 119/123] perf(headers): a single set-cookie (#2903) --- benchmarks/fetch/headers-length32.mjs | 2 +- lib/web/fetch/headers.js | 2 +- test/fetch/headers.js | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/benchmarks/fetch/headers-length32.mjs b/benchmarks/fetch/headers-length32.mjs index 744263f2569..a9b7a52a40e 100644 --- a/benchmarks/fetch/headers-length32.mjs +++ b/benchmarks/fetch/headers-length32.mjs @@ -19,7 +19,7 @@ const headers = new Headers( 'Width', 'Accept-CH', 'Via', - 'Refresh', + 'Set-Cookie', 'Server', 'Sec-Fetch-Dest', 'Sec-CH-UA-Model', diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index f21c33b438d..b3ec5a70711 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -536,7 +536,7 @@ class Headers { const cookies = this[kHeadersList].cookies // fast-path - if (cookies === null) { + if (cookies === null || cookies.length === 1) { // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray` return (this[kHeadersList][kHeadersSortedMap] = names) } diff --git a/test/fetch/headers.js b/test/fetch/headers.js index acdf5ab611e..00798559e90 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -694,7 +694,7 @@ test('Headers.prototype.getSetCookie', async (t) => { }) // https://github.com/nodejs/undici/issues/1935 - await t.test('When Headers are cloned, so are the cookies', async (t) => { + await t.test('When Headers are cloned, so are the cookies (single entry)', async (t) => { const server = createServer((req, res) => { res.setHeader('Set-Cookie', 'test=onetwo') res.end('Hello World!') @@ -709,6 +709,22 @@ test('Headers.prototype.getSetCookie', async (t) => { assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo']) assert.ok('set-cookie' in entries) }) + + await t.test('When Headers are cloned, so are the cookies (multiple entries)', async (t) => { + const server = createServer((req, res) => { + res.setHeader('Set-Cookie', ['test=onetwo', 'test=onetwothree']) + res.end('Hello World!') + }).listen(0) + + await once(server, 'listening') + t.after(closeServerAsPromise(server)) + + const res = await fetch(`http://localhost:${server.address().port}`) + const entries = Object.fromEntries(res.headers.entries()) + + assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo', 'test=onetwothree']) + assert.ok('set-cookie' in entries) + }) }) test('When the value is updated, update the cache', (t) => { From 2505e427f94beb132cc17bcbe6d883d0d75bac8f Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 3 Mar 2024 10:58:01 +0100 Subject: [PATCH 120/123] websocket: improve .close() (#2865) --- lib/web/websocket/connection.js | 6 +++--- lib/web/websocket/constants.js | 7 +++++++ lib/web/websocket/receiver.js | 6 +++--- lib/web/websocket/util.js | 14 ++++++++++++++ lib/web/websocket/websocket.js | 24 ++++++++++++++++++------ test/websocket/close.js | 25 ++++++++++++++++++++++++- 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 2869ebafb63..74674ee5ab7 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -1,6 +1,6 @@ 'use strict' -const { uid, states } = require('./constants') +const { uid, states, sentCloseFrameState } = require('./constants') const { kReadyState, kSentClose, @@ -230,7 +230,7 @@ function onSocketClose () { // If the TCP connection was closed after the // WebSocket closing handshake was completed, the WebSocket connection // is said to have been closed _cleanly_. - const wasClean = ws[kSentClose] && ws[kReceivedClose] + const wasClean = ws[kSentClose] === sentCloseFrameState.SENT && ws[kReceivedClose] let code = 1005 let reason = '' @@ -240,7 +240,7 @@ function onSocketClose () { if (result) { code = result.code ?? 1005 reason = result.reason - } else if (!ws[kSentClose]) { + } else if (ws[kSentClose] !== sentCloseFrameState.SENT) { // If _The WebSocket // Connection is Closed_ and no Close control frame was received by the // endpoint (such as could occur if the underlying transport connection diff --git a/lib/web/websocket/constants.js b/lib/web/websocket/constants.js index 406b8e3e2f0..d5de91460f5 100644 --- a/lib/web/websocket/constants.js +++ b/lib/web/websocket/constants.js @@ -20,6 +20,12 @@ const states = { CLOSED: 3 } +const sentCloseFrameState = { + NOT_SENT: 0, + PROCESSING: 1, + SENT: 2 +} + const opcodes = { CONTINUATION: 0x0, TEXT: 0x1, @@ -42,6 +48,7 @@ const emptyBuffer = Buffer.allocUnsafe(0) module.exports = { uid, + sentCloseFrameState, staticPropertyDescriptors, states, opcodes, diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index eff9aea569f..ab6ed0bce02 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -1,7 +1,7 @@ 'use strict' const { Writable } = require('node:stream') -const { parserStates, opcodes, states, emptyBuffer } = require('./constants') +const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants') const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols') const { channels } = require('../../core/diagnostics') const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util') @@ -104,7 +104,7 @@ class ByteParser extends Writable { this.#info.closeInfo = this.parseCloseBody(body) - if (!this.ws[kSentClose]) { + if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { // If an endpoint receives a Close frame and did not previously send a // Close frame, the endpoint MUST send a Close frame in response. (When // sending a Close frame in response, the endpoint typically echos the @@ -120,7 +120,7 @@ class ByteParser extends Writable { closeFrame.createFrame(opcodes.CLOSE), (err) => { if (!err) { - this.ws[kSentClose] = true + this.ws[kSentClose] = sentCloseFrameState.SENT } } ) diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index 8abe73c83e3..4f0e4dcf5dd 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -8,6 +8,17 @@ const { MessageEvent, ErrorEvent } = require('./events') /** * @param {import('./websocket').WebSocket} ws + * @returns {boolean} + */ +function isConnecting (ws) { + // If the WebSocket connection is not yet established, and the connection + // is not yet closed, then the WebSocket connection is in the CONNECTING state. + return ws[kReadyState] === states.CONNECTING +} + +/** + * @param {import('./websocket').WebSocket} ws + * @returns {boolean} */ function isEstablished (ws) { // If the server's response is validated as provided for above, it is @@ -18,6 +29,7 @@ function isEstablished (ws) { /** * @param {import('./websocket').WebSocket} ws + * @returns {boolean} */ function isClosing (ws) { // Upon either sending or receiving a Close control frame, it is said @@ -28,6 +40,7 @@ function isClosing (ws) { /** * @param {import('./websocket').WebSocket} ws + * @returns {boolean} */ function isClosed (ws) { return ws[kReadyState] === states.CLOSED @@ -190,6 +203,7 @@ function failWebsocketConnection (ws, reason) { } module.exports = { + isConnecting, isEstablished, isClosing, isClosed, diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index c08f02cbe38..c4b40b43188 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -3,7 +3,7 @@ const { webidl } = require('../fetch/webidl') const { URLSerializer } = require('../fetch/data-url') const { getGlobalOrigin } = require('../fetch/global') -const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants') +const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes, emptyBuffer } = require('./constants') const { kWebSocketURL, kReadyState, @@ -13,7 +13,15 @@ const { kSentClose, kByteParser } = require('./symbols') -const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util') +const { + isConnecting, + isEstablished, + isClosed, + isClosing, + isValidSubprotocol, + failWebsocketConnection, + fireEvent +} = require('./util') const { establishWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { ByteParser } = require('./receiver') @@ -132,6 +140,8 @@ class WebSocket extends EventTarget { // be CONNECTING (0). this[kReadyState] = WebSocket.CONNECTING + this[kSentClose] = sentCloseFrameState.NOT_SENT + // The extensions attribute must initially return the empty string. // The protocol attribute must initially return the empty string. @@ -184,7 +194,7 @@ class WebSocket extends EventTarget { } // 3. Run the first matching steps from the following list: - if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + if (isClosing(this) || isClosed(this)) { // If this's ready state is CLOSING (2) or CLOSED (3) // Do nothing. } else if (!isEstablished(this)) { @@ -193,7 +203,7 @@ class WebSocket extends EventTarget { // to CLOSING (2). failWebsocketConnection(this, 'Connection was closed before it was established.') this[kReadyState] = WebSocket.CLOSING - } else if (!isClosing(this)) { + } else if (this[kSentClose] === sentCloseFrameState.NOT_SENT) { // If the WebSocket closing handshake has not yet been started // Start the WebSocket closing handshake and set this's ready // state to CLOSING (2). @@ -204,6 +214,8 @@ class WebSocket extends EventTarget { // - If reason is also present, then reasonBytes must be // provided in the Close message after the status code. + this[kSentClose] = sentCloseFrameState.PROCESSING + const frame = new WebsocketFrameSend() // If neither code nor reason is present, the WebSocket Close @@ -230,7 +242,7 @@ class WebSocket extends EventTarget { socket.write(frame.createFrame(opcodes.CLOSE), (err) => { if (!err) { - this[kSentClose] = true + this[kSentClose] = sentCloseFrameState.SENT } }) @@ -258,7 +270,7 @@ class WebSocket extends EventTarget { // 1. If this's ready state is CONNECTING, then throw an // "InvalidStateError" DOMException. - if (this[kReadyState] === WebSocket.CONNECTING) { + if (isConnecting(this)) { throw new DOMException('Sent before connected.', 'InvalidStateError') } diff --git a/test/websocket/close.js b/test/websocket/close.js index cb8825645ba..c4305d60948 100644 --- a/test/websocket/close.js +++ b/test/websocket/close.js @@ -1,6 +1,7 @@ 'use strict' -const { describe, test } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') +const { describe, test, after } = require('node:test') const assert = require('node:assert') const { WebSocketServer } = require('ws') const { WebSocket } = require('../..') @@ -128,4 +129,26 @@ describe('Close', () => { ws.addEventListener('open', () => ws.close(3000)) }) }) + + test('calling close twice will only trigger the close event once', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = new WebSocketServer({ port: 0 }) + + after(() => server.close()) + + server.on('connection', (ws) => { + ws.on('close', (code) => { + t.strictEqual(code, 1000) + }) + }) + + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + ws.addEventListener('open', () => { + ws.close(1000) + ws.close(1000) + }) + + await t.completed + }) }) From 071609f8b1796b611a5bbf708ed85a6c4fd49466 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 3 Mar 2024 19:09:54 +0900 Subject: [PATCH 121/123] feat: add sending data benchmark (#2905) * feat: add post benchmark * fixup * apply suggestions from code review * apply suggestions from code review --- .github/workflows/bench.yml | 43 ++++ benchmarks/benchmark.js | 19 +- benchmarks/package.json | 5 +- benchmarks/post-benchmark.js | 422 +++++++++++++++++++++++++++++++++++ 4 files changed, 478 insertions(+), 11 deletions(-) create mode 100644 benchmarks/post-benchmark.js diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 5afcbddd807..112a98b6e72 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -49,3 +49,46 @@ jobs: - name: Run Benchmark run: npm run bench working-directory: ./benchmarks + + benchmark_post_current: + name: benchmark (sending data) current + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + ref: ${{ github.base_ref }} + - name: Setup Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: lts/* + - name: Install Modules for undici + run: npm i --ignore-scripts --omit=dev + - name: Install Modules for Benchmarks + run: npm i + working-directory: ./benchmarks + - name: Run Benchmark + run: npm run bench-post + working-directory: ./benchmarks + + benchmark_post_branch: + name: benchmark (sending data) branch + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + - name: Setup Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: lts/* + - name: Install Modules for undici + run: npm i --ignore-scripts --omit=dev + - name: Install Modules for Benchmarks + run: npm i + working-directory: ./benchmarks + - name: Run Benchmark + run: npm run bench-post + working-directory: ./benchmarks diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index 8775611138d..c48c00926a1 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -34,22 +34,16 @@ if (process.env.PORT) { dest.socketPath = path.join(os.tmpdir(), 'undici.sock') } +/** @type {http.RequestOptions} */ const httpBaseOptions = { protocol: 'http:', hostname: 'localhost', method: 'GET', path: '/', - query: { - frappucino: 'muffin', - goat: 'scone', - pond: 'moose', - foo: ['bar', 'baz', 'bal'], - bool: true, - numberKey: 256 - }, ...dest } +/** @type {http.RequestOptions} */ const httpNoKeepAliveOptions = { ...httpBaseOptions, agent: new http.Agent({ @@ -58,6 +52,7 @@ const httpNoKeepAliveOptions = { }) } +/** @type {http.RequestOptions} */ const httpKeepAliveOptions = { ...httpBaseOptions, agent: new http.Agent({ @@ -142,7 +137,11 @@ class SimpleRequest { } function makeParallelRequests (cb) { - return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) + const promises = new Array(parallelRequests) + for (let i = 0; i < parallelRequests; ++i) { + promises[i] = new Promise(cb) + } + return Promise.all(promises) } function printResults (results) { @@ -303,7 +302,7 @@ if (process.env.PORT) { experiments.got = () => { return makeParallelRequests(resolve => { - got.get(dest.url, null, { http: gotAgent }).then(res => { + got.get(dest.url, { agent: { http: gotAgent } }).then(res => { res.pipe(new Writable({ write (chunk, encoding, callback) { callback() diff --git a/benchmarks/package.json b/benchmarks/package.json index 5781ae37478..a834377135f 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -2,9 +2,12 @@ "name": "benchmarks", "scripts": { "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", + "bench-post": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench-post:run", "bench:server": "node ./server.js", "prebench:run": "node ./wait.js", - "bench:run": "SAMPLES=100 CONNECTIONS=50 node ./benchmark.js" + "bench:run": "SAMPLES=100 CONNECTIONS=50 node ./benchmark.js", + "prebench-post:run": "node ./wait.js", + "bench-post:run": "SAMPLES=100 CONNECTIONS=50 node ./post-benchmark.js" }, "dependencies": { "axios": "^1.6.7", diff --git a/benchmarks/post-benchmark.js b/benchmarks/post-benchmark.js new file mode 100644 index 00000000000..829fc389476 --- /dev/null +++ b/benchmarks/post-benchmark.js @@ -0,0 +1,422 @@ +'use strict' + +const http = require('node:http') +const os = require('node:os') +const path = require('node:path') +const { Writable } = require('node:stream') +const { isMainThread } = require('node:worker_threads') + +const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') + +let nodeFetch +const axios = require('axios') +let superagent +let got + +const util = require('node:util') +const _request = require('request') +const request = util.promisify(_request) + +const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1 +const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3 +const connections = parseInt(process.env.CONNECTIONS, 10) || 50 +const pipelining = parseInt(process.env.PIPELINING, 10) || 10 +const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100 +const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0 +const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0 +const dest = {} + +const data = '_'.repeat(128 * 1024) +const dataLength = `${Buffer.byteLength(data)}` + +if (process.env.PORT) { + dest.port = process.env.PORT + dest.url = `http://localhost:${process.env.PORT}` +} else { + dest.url = 'http://localhost' + dest.socketPath = path.join(os.tmpdir(), 'undici.sock') +} + +const headers = { + 'Content-Type': 'text/plain; charset=UTF-8', + 'Content-Length': dataLength +} + +/** @type {http.RequestOptions} */ +const httpBaseOptions = { + protocol: 'http:', + hostname: 'localhost', + method: 'POST', + path: '/', + headers, + ...dest +} + +/** @type {http.RequestOptions} */ +const httpNoKeepAliveOptions = { + ...httpBaseOptions, + agent: new http.Agent({ + keepAlive: false, + maxSockets: connections + }) +} + +/** @type {http.RequestOptions} */ +const httpKeepAliveOptions = { + ...httpBaseOptions, + agent: new http.Agent({ + keepAlive: true, + maxSockets: connections + }) +} + +const axiosAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + +const fetchAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + +const gotAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + +const requestAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + +const superagentAgent = new http.Agent({ + keepAlive: true, + maxSockets: connections +}) + +/** @type {import("..").Dispatcher.DispatchOptions} */ +const undiciOptions = { + path: '/', + method: 'POST', + headersTimeout, + bodyTimeout, + body: data, + headers +} + +const Class = connections > 1 ? Pool : Client +const dispatcher = new Class(httpBaseOptions.url, { + pipelining, + connections, + ...dest +}) + +setGlobalDispatcher(new Agent({ + pipelining, + connections, + connect: { + rejectUnauthorized: false + } +})) + +class SimpleRequest { + constructor (resolve) { + this.dst = new Writable({ + write (chunk, encoding, callback) { + callback() + } + }).on('finish', resolve) + } + + onConnect (abort) { } + + onHeaders (statusCode, headers, resume) { + this.dst.on('drain', resume) + } + + onData (chunk) { + return this.dst.write(chunk) + } + + onComplete () { + this.dst.end() + } + + onError (err) { + throw err + } +} + +function makeParallelRequests (cb) { + const promises = new Array(parallelRequests) + for (let i = 0; i < parallelRequests; ++i) { + promises[i] = new Promise(cb) + } + return Promise.all(promises) +} + +function printResults (results) { + // Sort results by least performant first, then compare relative performances and also printing padding + let last + + const rows = Object.entries(results) + // If any failed, put on the top of the list, otherwise order by mean, ascending + .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean)) + .map(([name, result]) => { + if (!result.success) { + return { + Tests: name, + Samples: result.size, + Result: 'Errored', + Tolerance: 'N/A', + 'Difference with Slowest': 'N/A' + } + } + + // Calculate throughput and relative performance + const { size, mean, standardError } = result + const relative = last !== 0 ? (last / mean - 1) * 100 : 0 + + // Save the slowest for relative comparison + if (typeof last === 'undefined') { + last = mean + } + + return { + Tests: name, + Samples: size, + Result: `${((parallelRequests * 1e9) / mean).toFixed(2)} req/sec`, + Tolerance: `± ${((standardError / mean) * 100).toFixed(2)} %`, + 'Difference with slowest': relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' + } + }) + + return console.table(rows) +} + +const experiments = { + 'http - no keepalive' () { + return makeParallelRequests(resolve => { + const request = http.request(httpNoKeepAliveOptions, res => { + res + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + request.end(data) + }) + }, + 'http - keepalive' () { + return makeParallelRequests(resolve => { + const request = http.request(httpKeepAliveOptions, res => { + res + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + request.end(data) + }) + }, + 'undici - pipeline' () { + return makeParallelRequests(resolve => { + dispatcher + .pipeline(undiciOptions, data => { + return data.body + }) + .end() + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }, + 'undici - request' () { + return makeParallelRequests(resolve => { + dispatcher.request(undiciOptions).then(({ body }) => { + body + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }) + }, + 'undici - stream' () { + return makeParallelRequests(resolve => { + return dispatcher + .stream(undiciOptions, () => { + return new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + }) + .then(resolve) + }) + }, + 'undici - dispatch' () { + return makeParallelRequests(resolve => { + dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) + }) + } +} + +if (process.env.PORT) { + /** @type {RequestInit} */ + const fetchOptions = { + method: 'POST', + body: data, + headers + } + // fetch does not support the socket + experiments['undici - fetch'] = () => { + return makeParallelRequests(resolve => { + fetch(dest.url, fetchOptions).then(res => { + res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } })) + }).catch(console.log) + }) + } + + const nodeFetchOptions = { + ...fetchOptions, + agent: fetchAgent + } + experiments['node-fetch'] = () => { + return makeParallelRequests(resolve => { + nodeFetch(dest.url, nodeFetchOptions).then(res => { + res.body.pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }).catch(console.log) + }) + } + + const axiosOptions = { + url: dest.url, + method: 'POST', + headers, + responseType: 'stream', + httpAgent: axiosAgent, + data + } + experiments.axios = () => { + return makeParallelRequests(resolve => { + axios.request(axiosOptions).then(res => { + res.data.pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }).catch(console.log) + }) + } + + const gotOptions = { + method: 'POST', + headers, + agent: { + http: gotAgent + }, + body: data + } + experiments.got = () => { + return makeParallelRequests(resolve => { + got(dest.url, gotOptions).then(res => { + res.pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }).catch(console.log) + }) + } + + const requestOptions = { + url: dest.url, + method: 'POST', + headers, + agent: requestAgent, + data + } + experiments.request = () => { + return makeParallelRequests(resolve => { + request(requestOptions).then(res => { + res.pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }).catch(console.log) + }) + } + + experiments.superagent = () => { + return makeParallelRequests(resolve => { + superagent + .post(dest.url) + .send(data) + .set('Content-Type', 'text/plain; charset=UTF-8') + .set('Content-Length', dataLength) + .pipe(new Writable({ + write (chunk, encoding, callback) { + callback() + } + })).on('finish', resolve) + }) + } +} + +async function main () { + const { cronometro } = await import('cronometro') + const _nodeFetch = await import('node-fetch') + nodeFetch = _nodeFetch.default + const _got = await import('got') + got = _got.default + const _superagent = await import('superagent') + // https://github.com/ladjs/superagent/issues/1540#issue-561464561 + superagent = _superagent.agent().use((req) => req.agent(superagentAgent)) + + cronometro( + experiments, + { + iterations, + errorThreshold, + print: false + }, + (err, results) => { + if (err) { + throw err + } + + printResults(results) + dispatcher.destroy() + } + ) +} + +if (isMainThread) { + main() +} else { + module.exports = main +} From 0f585700a8633f945d056dcdfa48f7066373bde4 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 3 Mar 2024 14:10:18 +0100 Subject: [PATCH 122/123] ci: integrate workflows into nodejs.yml (#2899) --- .github/workflows/dependency-review.yml | 27 -------- .github/workflows/lint.yml | 17 ----- .github/workflows/nodejs.yml | 84 ++++++++++++++++++++++--- package.json | 4 +- 4 files changed, 77 insertions(+), 55 deletions(-) delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 55928d96318..00000000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, -# surfacing known-vulnerable versions of the packages declared or updated in the PR. -# Once installed, if the workflow run is marked as required, -# PRs introducing known-vulnerable packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -name: 'Dependency Review' -on: [pull_request] - -permissions: - contents: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 - with: - egress-policy: audit - - - name: 'Checkout Repository' - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: 'Dependency Review' - uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 06b1ab1cca2..00000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Lint -on: [push, pull_request] -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - persist-credentials: false - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: lts/* - - run: npm install - - run: npm run lint diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 598220e662d..1e6e6d593ed 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,8 +9,49 @@ on: - 'v*' pull_request: +permissions: + contents: read + jobs: + dependency-review: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: Dependency Review + uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install + + - name: Lint + run: npm run lint + test: + name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} timeout-minutes: 15 strategy: fail-fast: false @@ -25,16 +66,14 @@ jobs: - windows-latest runs-on: ${{ matrix.runs-on }} - steps: - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: Setup Node.js@${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.node-version }} @@ -63,15 +102,42 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + test-types: + name: Test TypeScript types + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install + + - name: Run typings tests + run: npm run test:typescript + automerge: if: > github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' - needs: test + needs: + - dependency-review + - test + - test-types + - lint runs-on: ubuntu-latest permissions: - pull-requests: write contents: write + pull-requests: write + actions: write steps: - - uses: fastify/github-action-merge-dependabot@9e7bfb249c69139d7bdcd8d984f9665edd49020b # v3.10.1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Merge Dependabot PR + uses: fastify/github-action-merge-dependabot@9e7bfb249c69139d7bdcd8d984f9665edd49020b # v3.10.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index 8fdfd7a4bc4..aca3f94ce28 100644 --- a/package.json +++ b/package.json @@ -81,8 +81,8 @@ "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts", "test:websocket": "borp -p \"test/websocket/*.js\"", "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", - "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test && npm run coverage:report", - "coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test && npm run coverage:report:ci", + "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report", + "coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci", "coverage:clean": "node ./scripts/clean-coverage.js", "coverage:report": "cross-env NODE_V8_COVERAGE= c8 report", "coverage:report:ci": "c8 report", From 2316bae1b790517b9fbc8d066582410604ab733b Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 3 Mar 2024 18:08:17 +0100 Subject: [PATCH 123/123] Bumepd v6.7.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aca3f94ce28..a618b628107 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.6.2", + "version": "6.7.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": {