From d1e4add8014ac27c2031f41d12330cc54fb3a971 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 27 Jun 2024 00:50:22 +0800 Subject: [PATCH] feat: support HTTP2 (#516) closes https://github.com/node-modules/urllib/issues/474 --- README.md | 16 ++++++++++ package.json | 2 +- src/HttpAgent.ts | 3 +- src/HttpClient.ts | 9 ++++++ test/HttpClient.test.ts | 70 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 97 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf521dfc..e9e0bd4d 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/package.json b/package.json index bb51ae8b..426ffd8b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 180000 keep-alive-header.test.ts", "cov": "vitest run --coverage", "preci": "node scripts/pre_test.js", - "ci": "npm run lint && npm run cov && node scripts/build_test.js && attw --pack", + "ci": "npm run lint && npm run cov && node scripts/build_test.js && attw --pack --ignore-rules no-resolution", "contributor": "git-contributor", "clean": "rm -rf dist", "prepublishOnly": "npm run build" 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 5bf5e7d2..f7120a80 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -70,6 +70,8 @@ const isNode14Or16 = /v1[46]\./.test(process.version); export type ClientOptions = { defaultArgs?: RequestOptions; + /** Allow to use HTTP2 first. Default is `false` */ + allowH2?: boolean; /** * Custom DNS lookup function, default is `dns.lookup`. */ @@ -187,10 +189,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 07ecdacc..ff1bf204 100644 --- a/test/HttpClient.test.ts +++ b/test/HttpClient.test.ts @@ -1,7 +1,8 @@ import { strict as assert } from 'node:assert'; import dns from 'node:dns'; +import { sensitiveHeaders } from 'node:http2'; import { describe, it, beforeAll, afterAll } from 'vitest'; -import { HttpClient } from '../src'; +import { HttpClient, getGlobalDispatcher } from '../src'; import { RawResponseWithMeta } from '../src/Response'; import { startServer } from './fixtures/server'; @@ -28,6 +29,73 @@ describe('HttpClient.test.ts', () => { }); }); + describe('clientOptions.allowH2', () => { + it('should work with allowH2 = true', async () => { + const httpClient = new HttpClient({ + allowH2: true, + }); + assert(httpClient); + let 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'); + 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'); + 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 } });