From 21d4260410ca7ca53bc60af34a2f86dc9b4f8e8a Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 27 Jun 2024 09:40:58 +0800 Subject: [PATCH] feat: support HTTP2 (#518) closes https://github.com/node-modules/urllib/issues/474 pick from https://github.com/node-modules/urllib/pull/516 ## Summary by CodeRabbit - **New Features** - Introduced HTTP/2 support in `HttpClient` with the new `allowH2` option. - Added `getGlobalDispatcher` function for managing global dispatchers. - **Documentation** - Updated README with a new section on making requests using HTTP/2 in `HttpClient`. - **Tests** - Added test cases for the `allowH2` option in `HttpClient`. - **Chores** - Updated Node.js versions in GitHub Actions configuration to include additional versions and correct an existing version. --- .github/workflows/nodejs.yml | 2 +- README.md | 16 +++++++ src/HttpAgent.ts | 3 +- src/HttpClient.ts | 9 ++++ test/HttpClient.test.ts | 90 +++++++++++++++++++++++++++++++++++- 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 271cd4b1..c70132da 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,6 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest, macos-latest, windows-latest' - version: '18.19.0, 20, 22' + version: '18.19.0, 18, 20, 22' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index d2fc90a4..97abad64 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,22 @@ Response is normal object, it contains: NODE_DEBUG=urllib:* npm test ``` +## Request with HTTP2 + +Create a HttpClient with `options.allowH2 = true` + +```ts +import { HttpClient } from 'urllib'; + +const httpClient = new HttpClient({ + allowH2: true, +}); + +const response = await httpClient.request('https://node.js.org'); +console.log(response.status); +console.log(response.headers); +``` + ## Mocking Request export from [undici](https://undici.nodejs.org/#/docs/best-practices/mocking-request) diff --git a/src/HttpAgent.ts b/src/HttpAgent.ts index 0fff0886..54feaa46 100644 --- a/src/HttpAgent.ts +++ b/src/HttpAgent.ts @@ -12,6 +12,7 @@ export type HttpAgentOptions = { lookup?: LookupFunction; checkAddress?: CheckAddressFunction; connect?: buildConnector.BuildOptions, + allowH2?: boolean; }; class IllegalAddressError extends Error { @@ -62,7 +63,7 @@ export class HttpAgent extends Agent { }); }; super({ - connect: { ...options.connect, lookup }, + connect: { ...options.connect, lookup, allowH2: options.allowH2 }, }); this.#checkAddress = options.checkAddress; } diff --git a/src/HttpClient.ts b/src/HttpClient.ts index dad15287..02cd4962 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -57,6 +57,8 @@ const debug = debuglog('urllib:HttpClient'); export type ClientOptions = { defaultArgs?: RequestOptions; + /** Allow to use HTTP2 first. Default is `false` */ + allowH2?: boolean; /** * Custom DNS lookup function, default is `dns.lookup`. */ @@ -177,10 +179,17 @@ export class HttpClient extends EventEmitter { lookup: clientOptions.lookup, checkAddress: clientOptions.checkAddress, connect: clientOptions.connect, + allowH2: clientOptions.allowH2, }); } else if (clientOptions?.connect) { this.#dispatcher = new Agent({ connect: clientOptions.connect, + allowH2: clientOptions.allowH2, + }); + } else if (clientOptions?.allowH2) { + // Support HTTP2 + this.#dispatcher = new Agent({ + allowH2: clientOptions.allowH2, }); } initDiagnosticsChannel(); diff --git a/test/HttpClient.test.ts b/test/HttpClient.test.ts index c9e02825..b6abed23 100644 --- a/test/HttpClient.test.ts +++ b/test/HttpClient.test.ts @@ -1,9 +1,23 @@ import { strict as assert } from 'node:assert'; import dns from 'node:dns'; +import { sensitiveHeaders } from 'node:http2'; +import { PerformanceObserver } from 'node:perf_hooks'; import { describe, it, beforeAll, afterAll } from 'vitest'; -import { HttpClient, RawResponseWithMeta } from '../src/index.js'; +import { HttpClient, RawResponseWithMeta, getGlobalDispatcher } from '../src/index.js'; import { startServer } from './fixtures/server.js'; +if (process.env.ENABLE_PERF) { + const obs = new PerformanceObserver(items => { + items.getEntries().forEach(item => { + console.log('%j', item); + }); + }); + obs.observe({ + entryTypes: [ 'net', 'dns', 'function', 'gc', 'http', 'http2', 'node' ], + buffered: true, + }); +} + describe('HttpClient.test.ts', () => { let close: any; let _url: string; @@ -27,6 +41,80 @@ describe('HttpClient.test.ts', () => { }); }); + describe('clientOptions.allowH2', () => { + it('should work with allowH2 = true', async () => { + const httpClient = new HttpClient({ + allowH2: true, + }); + const httpClient1 = new HttpClient({ + allowH2: false, + }); + let response = await httpClient.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + console.log(response.res.socket, response.res.timing); + response = await httpClient1.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + console.log(response.res.socket, response.res.timing); + // assert.equal(sensitiveHeaders in response.headers, true); + assert.equal(response.headers['content-type'], 'application/json; charset=utf-8'); + assert.notEqual(httpClient.getDispatcher(), getGlobalDispatcher()); + response = await httpClient.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + // assert.equal(sensitiveHeaders in response.headers, true); + assert.equal(response.headers['content-type'], 'application/json; charset=utf-8'); + response = await httpClient.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + // assert.equal(sensitiveHeaders in response.headers, true); + assert.equal(response.headers['content-type'], 'application/json; charset=utf-8'); + response = await httpClient.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + // assert.equal(sensitiveHeaders in response.headers, true); + assert.equal(response.headers['content-type'], 'application/json; charset=utf-8'); + response = await httpClient.request('https://registry.npmmirror.com/urllib'); + assert.equal(response.status, 200); + // assert.equal(sensitiveHeaders in response.headers, true); + assert.equal(response.headers['content-type'], 'application/json; charset=utf-8'); + console.log(response.res.socket, response.res.timing); + await Promise.all([ + httpClient.request('https://registry.npmmirror.com/urllib'), + httpClient.request('https://registry.npmmirror.com/urllib'), + httpClient.request('https://registry.npmmirror.com/urllib'), + httpClient.request('https://registry.npmmirror.com/urllib'), + ]); + + // should request http 1.1 server work + let response2 = await httpClient.request(_url); + assert.equal(response2.status, 200); + assert.equal(sensitiveHeaders in response2.headers, false); + assert.equal(response2.headers['content-type'], 'application/json'); + response2 = await httpClient.request(_url); + assert.equal(response2.status, 200); + assert.equal(sensitiveHeaders in response2.headers, false); + assert.equal(response2.headers['content-type'], 'application/json'); + response2 = await httpClient.request(_url); + assert.equal(response2.status, 200); + assert.equal(sensitiveHeaders in response2.headers, false); + assert.equal(response2.headers['content-type'], 'application/json'); + response2 = await httpClient.request(_url); + assert.equal(response2.status, 200); + assert.equal(sensitiveHeaders in response2.headers, false); + assert.equal(response2.headers['content-type'], 'application/json'); + response2 = await httpClient.request(_url); + assert.equal(response2.status, 200); + assert.equal(sensitiveHeaders in response2.headers, false); + assert.equal(response2.headers['content-type'], 'application/json'); + await Promise.all([ + httpClient.request(_url), + httpClient.request(_url), + httpClient.request(_url), + httpClient.request(_url), + ]); + console.log(httpClient.getDispatcherPoolStats()); + assert.equal(httpClient.getDispatcherPoolStats()['https://registry.npmmirror.com'].connected, 1); + assert(httpClient.getDispatcherPoolStats()[_url.substring(0, _url.length - 1)].connected > 1); + }); + }); + describe('clientOptions.defaultArgs', () => { it('should work with custom defaultArgs', async () => { const httpclient = new HttpClient({ defaultArgs: { timeout: 1000 } });