Skip to content

Commit

Permalink
feat: support HTTP2 (#518)
Browse files Browse the repository at this point in the history
closes #474

pick from #516

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
fengmk2 authored Jun 27, 2024
1 parent 8247aa2 commit 21d4260
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/HttpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type HttpAgentOptions = {
lookup?: LookupFunction;
checkAddress?: CheckAddressFunction;
connect?: buildConnector.BuildOptions,
allowH2?: boolean;
};

class IllegalAddressError extends Error {
Expand Down Expand Up @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions src/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -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();
Expand Down
90 changes: 89 additions & 1 deletion test/HttpClient.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 } });
Expand Down

0 comments on commit 21d4260

Please sign in to comment.