From fde0d23cb8fd19b9ccc36ce1bdc3ec93392d48b8 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 4 Dec 2023 09:34:17 +0800 Subject: [PATCH] feat: allow to set client connect timeout (#476) closes https://github.com/node-modules/urllib/issues/466 --- README.md | 17 ++++---- examples/timing.cjs | 9 +++- src/HttpClient.ts | 21 ++++++++- src/index.ts | 4 +- test/index.test.ts | 104 ++++++++++++++++++++++++++++++++++++++++++-- vite.config.ts | 1 + 6 files changed, 138 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d37bcc61..a8398db3 100644 --- a/README.md +++ b/README.md @@ -290,14 +290,15 @@ Fork [undici benchmarks script](https://github.com/fengmk2/undici/blob/urllib-be |[
fengmk2](https://github.com/fengmk2)
|[
dead-horse](https://github.com/dead-horse)
|[
semantic-release-bot](https://github.com/semantic-release-bot)
|[
xingrz](https://github.com/xingrz)
|[
popomore](https://github.com/popomore)
|[
JacksonTian](https://github.com/JacksonTian)
| | :---: | :---: | :---: | :---: | :---: | :---: | -|[
ibigbug](https://github.com/ibigbug)
|[
greenkeeperio-bot](https://github.com/greenkeeperio-bot)
|[
atian25](https://github.com/atian25)
|[
killagu](https://github.com/killagu)
|[
paambaati](https://github.com/paambaati)
|[
denghongcai](https://github.com/denghongcai)
| -|[
gemwuu](https://github.com/gemwuu)
|[
XadillaX](https://github.com/XadillaX)
|[
alsotang](https://github.com/alsotang)
|[
leoner](https://github.com/leoner)
|[
hyj1991](https://github.com/hyj1991)
|[
isayme](https://github.com/isayme)
| -|[
cyjake](https://github.com/cyjake)
|[
whxaxes](https://github.com/whxaxes)
|[
chadxz](https://github.com/chadxz)
|[
adapt0](https://github.com/adapt0)
|[
danielwpz](https://github.com/danielwpz)
|[
danielsss](https://github.com/danielsss)
| -|[
Jeff-Tian](https://github.com/Jeff-Tian)
|[
nick-ng](https://github.com/nick-ng)
|[
rishavsharan](https://github.com/rishavsharan)
|[
willizm](https://github.com/willizm)
|[
davidkhala](https://github.com/davidkhala)
|[
aleafs](https://github.com/aleafs)
| -|[
Amunu](https://github.com/Amunu)
|[
azure-pipelines[bot]](https://github.com/apps/azure-pipelines)
|[
capsice](https://github.com/capsice)
|[
changzhiwin](https://github.com/changzhiwin)
|[
yuzhigang33](https://github.com/yuzhigang33)
|[
elrrrrrrr](https://github.com/elrrrrrrr)
| -[
fishbar](https://github.com/fishbar)
|[
gxcsoccer](https://github.com/gxcsoccer)
|[
mars-coder](https://github.com/mars-coder)
|[
rockdai](https://github.com/rockdai)
|[
dickeylth](https://github.com/dickeylth)
|[
aladdin-add](https://github.com/aladdin-add)
- -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sat Sep 16 2023 01:02:26 GMT+0800`. +|[
ibigbug](https://github.com/ibigbug)
|[
greenkeeperio-bot](https://github.com/greenkeeperio-bot)
|[
atian25](https://github.com/atian25)
|[
killagu](https://github.com/killagu)
|[
paambaati](https://github.com/paambaati)
|[
tremby](https://github.com/tremby)
| +|[
denghongcai](https://github.com/denghongcai)
|[
gemwuu](https://github.com/gemwuu)
|[
XadillaX](https://github.com/XadillaX)
|[
alsotang](https://github.com/alsotang)
|[
leoner](https://github.com/leoner)
|[
hyj1991](https://github.com/hyj1991)
| +|[
isayme](https://github.com/isayme)
|[
cyjake](https://github.com/cyjake)
|[
whxaxes](https://github.com/whxaxes)
|[
chadxz](https://github.com/chadxz)
|[
adapt0](https://github.com/adapt0)
|[
danielwpz](https://github.com/danielwpz)
| +|[
danielsss](https://github.com/danielsss)
|[
Jeff-Tian](https://github.com/Jeff-Tian)
|[
nick-ng](https://github.com/nick-ng)
|[
rishavsharan](https://github.com/rishavsharan)
|[
willizm](https://github.com/willizm)
|[
davidkhala](https://github.com/davidkhala)
| +|[
aleafs](https://github.com/aleafs)
|[
Amunu](https://github.com/Amunu)
|[
azure-pipelines[bot]](https://github.com/apps/azure-pipelines)
|[
capsice](https://github.com/capsice)
|[
changzhiwin](https://github.com/changzhiwin)
|[
yuzhigang33](https://github.com/yuzhigang33)
| +|[
elrrrrrrr](https://github.com/elrrrrrrr)
|[
fishbar](https://github.com/fishbar)
|[
gxcsoccer](https://github.com/gxcsoccer)
|[
mars-coder](https://github.com/mars-coder)
|[
rockdai](https://github.com/rockdai)
|[
dickeylth](https://github.com/dickeylth)
| +[
aladdin-add](https://github.com/aladdin-add)
+ +This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Mon Dec 04 2023 00:13:39 GMT+0800`. diff --git a/examples/timing.cjs b/examples/timing.cjs index cd77ffef..c82e4b56 100644 --- a/examples/timing.cjs +++ b/examples/timing.cjs @@ -1,5 +1,10 @@ -const urllib = require('..'); +const { HttpClient } = require('..'); +const httpClient = new HttpClient({ + connect: { + timeout: 1500, + }, +}); const url = process.argv[2] || 'https://cnodejs.org'; console.log('timing: %s', url); @@ -9,7 +14,7 @@ async function request(index) { if (index === count) { return; } - const res = await urllib.request(url + '?index=' + index, { + const res = await httpClient.request(url + '?index=' + index, { // data: { wd: 'nodejs' }, dataType: 'json', }); diff --git a/src/HttpClient.ts b/src/HttpClient.ts index de69ebf3..58264f75 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -20,6 +20,8 @@ import { FormData as FormDataNative, request as undiciRequest, Dispatcher, + Agent, + getGlobalDispatcher, } from 'undici'; import { FormData as FormDataNode } from 'formdata-node'; import { FormDataEncoder } from 'form-data-encoder'; @@ -86,11 +88,14 @@ export type ClientOptions = { * An 'error' event is emitted if verification fails.Default: true. */ rejectUnauthorized?: boolean; - /** * socketPath string | null (optional) - Default: null - An IPC endpoint, either Unix domain socket or Windows named pipe */ socketPath?: string | null; + /** + * connect timeout, default is 10000ms + */ + timeout?: number; }, }; @@ -168,16 +173,28 @@ export class HttpClient extends EventEmitter { constructor(clientOptions?: ClientOptions) { super(); this.#defaultArgs = clientOptions?.defaultArgs; - if (clientOptions?.lookup || clientOptions?.checkAddress || clientOptions?.connect) { + if (clientOptions?.lookup || clientOptions?.checkAddress) { this.#dispatcher = new HttpAgent({ lookup: clientOptions.lookup, checkAddress: clientOptions.checkAddress, connect: clientOptions.connect, }); + } else if (clientOptions?.connect) { + this.#dispatcher = new Agent({ + connect: clientOptions.connect, + }); } initDiagnosticsChannel(); } + getDispatcher() { + return this.#dispatcher ?? getGlobalDispatcher(); + } + + setDispatcher(dispatcher: Dispatcher) { + this.#dispatcher = dispatcher; + } + async request(url: RequestURL, options?: RequestOptions) { return await this.#requestInternal(url, options); } diff --git a/src/index.ts b/src/index.ts index b7781b9e..7842b8be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { RequestOptions, RequestURL } from './Request.js'; let httpclient: HttpClient; const domainSocketHttpclients = new LRU(50); + export async function request(url: RequestURL, options?: RequestOptions) { if (options?.socketPath) { let domainSocketHttpclient = domainSocketHttpclients.get(options.socketPath); @@ -22,7 +23,7 @@ export async function request(url: RequestURL, options?: RequestOptions return await httpclient.request(url, options); } -// export curl method is keep compatible with urlib.curl() +// export curl method is keep compatible with urllib.curl() // ```ts // import * as urllib from 'urllib'; // urllib.curl(url); @@ -53,7 +54,6 @@ export { IncomingHttpHeaders, } from './IncomingHttpHeaders.js'; - export default { request, curl, diff --git a/test/index.test.ts b/test/index.test.ts index 6fbe131b..630313f3 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,8 +1,8 @@ import { strict as assert } from 'node:assert'; import { parse as urlparse } from 'node:url'; import { readFileSync } from 'node:fs'; -import { describe, it, beforeAll, afterAll } from 'vitest'; -import urllib from '../src'; +import { describe, it, beforeAll, afterAll, afterEach, beforeEach } from 'vitest'; +import urllib, { HttpClient } from '../src'; import { MockAgent, setGlobalDispatcher, getGlobalDispatcher } from '../src'; import { startServer } from './fixtures/server'; import { readableToBytes } from './utils'; @@ -162,12 +162,12 @@ describe('index.test.ts', () => { describe('Mocking request', () => { let mockAgent: MockAgent; const globalAgent = getGlobalDispatcher(); - beforeAll(() => { + beforeEach(() => { mockAgent = new MockAgent(); setGlobalDispatcher(mockAgent); }); - afterAll(async () => { + afterEach(async () => { setGlobalDispatcher(globalAgent); await mockAgent.close(); }); @@ -275,5 +275,101 @@ describe('index.test.ts', () => { mockAgent.assertNoPendingInterceptors(); }); + + it('should mocking intercept work on custom httpClient', async () => { + const httpClient = new HttpClient({ + connect: { + timeout: 2000, + }, + }); + const oldAgent = httpClient.getDispatcher(); + assert(oldAgent); + httpClient.setDispatcher(mockAgent); + const mockPool = mockAgent.get(_url.substring(0, _url.length - 1)); + mockPool.intercept({ + path: '/foo', + method: 'POST', + }).reply(400, { + message: 'mock 400 bad request', + }); + + mockPool.intercept({ + path: '/bar', + method: 'GET', + query: { + q: '1', + }, + }).reply(200, { + message: 'mock bar with q=1', + }); + + mockPool.intercept({ + path: '/bar', + method: 'GET', + query: { + q: '2', + }, + }).reply(200, { + message: 'mock bar with q=2', + }); + + mockPool.intercept({ + path: /\.tgz$/, + method: 'GET', + }).reply(400, { + message: 'mock 400 bad request on tgz', + }); + + let response = await httpClient.request(`${_url}foo`, { + method: 'POST', + dataType: 'json', + }); + assert.equal(response.status, 400); + assert.deepEqual(response.data, { message: 'mock 400 bad request' }); + + response = await httpClient.request(`${_url}bar?q=1`, { + method: 'GET', + dataType: 'json', + }); + assert.equal(response.status, 200); + assert.deepEqual(response.data, { message: 'mock bar with q=1' }); + response = await httpClient.request(`${_url}bar?q=2`, { + method: 'GET', + dataType: 'json', + }); + assert.equal(response.status, 200); + assert.deepEqual(response.data, { message: 'mock bar with q=2' }); + + response = await httpClient.request(`${_url}download/foo.tgz`, { + method: 'GET', + dataType: 'json', + }); + assert.equal(response.status, 400); + assert.deepEqual(response.data, { message: 'mock 400 bad request on tgz' }); + + // only intercept once + response = await httpClient.request(`${_url}download/bar.tgz`, { + method: 'GET', + dataType: 'json', + }); + assert.equal(response.status, 200); + assert.equal(response.data.method, 'GET'); + + mockAgent.assertNoPendingInterceptors(); + + // should not work + httpClient.setDispatcher(oldAgent); + mockPool.intercept({ + path: '/foo', + method: 'POST', + }).reply(400, { + message: 'mock 400 bad request', + }); + response = await httpClient.request(`${_url}foo`, { + method: 'POST', + dataType: 'json', + }); + assert.equal(response.status, 200); + }); }); }); diff --git a/vite.config.ts b/vite.config.ts index 188f19ce..4f9437eb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,5 +11,6 @@ export default defineConfig({ 'src', ], }, + pool: 'forks', }, });