From a8c5aafb21b19f73e45e5ac21034d40d5bd50002 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 28 Nov 2024 12:02:02 +0800 Subject: [PATCH] feat: upgrade to undici v7 fix server side close unexpected exit closes https://github.com/node-modules/urllib/issues/541 --- .../h2-other-side-closed-exit-0-fetch.cjs | 34 +++++++++++++ examples/h2-other-side-closed-exit-0.cjs | 34 +++++++++++++ examples/longruning.cjs | 49 +++++++++++++++++++ package.json | 2 +- src/FetchOpaqueInterceptor.ts | 2 +- src/HttpAgent.ts | 2 +- src/HttpClient.ts | 10 +++- src/fetch.ts | 13 +++-- 8 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 examples/h2-other-side-closed-exit-0-fetch.cjs create mode 100644 examples/h2-other-side-closed-exit-0.cjs create mode 100644 examples/longruning.cjs diff --git a/examples/h2-other-side-closed-exit-0-fetch.cjs b/examples/h2-other-side-closed-exit-0-fetch.cjs new file mode 100644 index 00000000..00b3b6b5 --- /dev/null +++ b/examples/h2-other-side-closed-exit-0-fetch.cjs @@ -0,0 +1,34 @@ +const { fetch, setGlobalDispatcher, Agent } = require('..'); + +setGlobalDispatcher(new Agent({ + allowH2: true, +})); + +async function main() { + for (let i = 0; i < 100; i++) { + try { + const r = await fetch('https://edgeupdates.microsoft.com/api/products'); + console.log(r.status, r.headers, (await r.text()).length); + } catch (err) { + // console.error(err); + // throw err; + if (err.code === 'UND_ERR_SOCKET') { + continue; + } else { + throw err; + } + } + } +} + +main().then(() => { + console.log('main end'); +}).catch(err => { + console.error('main error throw: %s', err); + // console.error(err); + process.exit(1); +}); + +process.on('beforeExit', (...args) => { + console.error('beforeExit', args); +}); diff --git a/examples/h2-other-side-closed-exit-0.cjs b/examples/h2-other-side-closed-exit-0.cjs new file mode 100644 index 00000000..a278ffe2 --- /dev/null +++ b/examples/h2-other-side-closed-exit-0.cjs @@ -0,0 +1,34 @@ +const { request, Agent, setGlobalDispatcher } = require('undici'); + +setGlobalDispatcher(new Agent({ + allowH2: true, +})); + +async function main() { + for (let i = 0; i < 100; i++) { + try { + const r = await request('https://edgeupdates.microsoft.com/api/products'); + console.log(r.statusCode, r.headers, (await r.body.blob()).size); + } catch (err) { + // console.error(err); + // throw err; + if (err.code === 'UND_ERR_SOCKET') { + continue; + } else { + throw err; + } + } + } +} + +main().then(() => { + console.log('main end'); +}).catch(err => { + console.error('main error throw: %s', err); + // console.error(err); + process.exit(1); +}); + +process.on('beforeExit', (...args) => { + console.error('beforeExit', args); +}); diff --git a/examples/longruning.cjs b/examples/longruning.cjs new file mode 100644 index 00000000..1db2b8ce --- /dev/null +++ b/examples/longruning.cjs @@ -0,0 +1,49 @@ +const { HttpClient } = require('..'); + +const httpClient = new HttpClient({ + allowH2: true, +}); + +async function main() { + for (let i = 0; i < 1000000; i++) { + // await httpClient.request('https://registry.npmmirror.com/'); + // console.log(r.status, r.headers, r.res.timing); + try { + const r = await httpClient.request('https://edgeupdates.microsoft.com/api/products'); + // console.log(r.status, r.headers, r.data.length, r.res.timing); + if (i % 10 === 0) { + // console.log(r.status, r.headers, r.data.length, r.res.timing); + console.log(i, r.status, process.memoryUsage()); + } + } catch (err) { + console.error('%s error: %s', i, err.message); + } + } +} + +main().then(() => { + console.log('main end'); +}).catch(err => { + console.error('main error throw: %s', err); + console.error(err); + process.exit(1); +}); + +// process.on('uncaughtException', (...args) => { +// console.error('uncaughtException', args); +// process.exit(1); +// }); + +// process.on('unhandledRejection', (...args) => { +// console.error('unhandledRejection', args); +// process.exit(2); +// }); + +// process.on('uncaughtExceptionMonitor', (...args) => { +// console.error('uncaughtExceptionMonitor', args); +// process.exit(2); +// }); + +process.on('beforeExit', (...args) => { + console.error('beforeExit', args); +}); diff --git a/package.json b/package.json index 5e330e01..c333d248 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "mime-types": "^2.1.35", "qs": "^6.12.1", "type-fest": "^4.20.1", - "undici": "^6.19.2", + "undici": "^7.0.0", "ylru": "^2.0.0" }, "devDependencies": { diff --git a/src/FetchOpaqueInterceptor.ts b/src/FetchOpaqueInterceptor.ts index 2a8564cd..dd741df5 100644 --- a/src/FetchOpaqueInterceptor.ts +++ b/src/FetchOpaqueInterceptor.ts @@ -32,7 +32,7 @@ export interface OpaqueInterceptorOptions { export function fetchOpaqueInterceptor(opts: OpaqueInterceptorOptions) { const opaqueLocalStorage = opts?.opaqueLocalStorage; return (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] => { - return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) { + return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler) { const opaque = opaqueLocalStorage?.getStore(); (handler as any).opaque = opaque; return dispatch(opts, handler); diff --git a/src/HttpAgent.ts b/src/HttpAgent.ts index 5b38121d..cb1200bf 100644 --- a/src/HttpAgent.ts +++ b/src/HttpAgent.ts @@ -70,7 +70,7 @@ export class HttpAgent extends Agent { this.#checkAddress = options.checkAddress; } - dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean { + dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean { if (this.#checkAddress && options.origin) { const originUrl = typeof options.origin === 'string' ? new URL(options.origin) : options.origin; let hostname = originUrl.hostname; diff --git a/src/HttpClient.ts b/src/HttpClient.ts index f05d5b52..af4d0969 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -49,7 +49,7 @@ type IUndiciRequestOption = PropertyShouldBe 0 && requestContext.socketErrorRetries < args.socketErrorRetry) { requestContext.socketErrorRetries++; + debug('Request#%d retry on socket error, socketErrorRetries: %d', + requestId, requestContext.socketErrorRetries); return await this.#requestInternal(url, options, requestContext); } } diff --git a/src/fetch.ts b/src/fetch.ts index d7c197c3..5c51b083 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -8,21 +8,20 @@ import { Agent, getGlobalDispatcher, Pool, - Dispatcher, } from 'undici'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import undiciSymbols from 'undici/lib/core/symbols.js'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import undiciFetchSymbols from 'undici/lib/web/fetch/symbols.js'; +import { getResponseState } from 'undici/lib/web/fetch/response.js'; import { channels, ClientOptions, PoolStat, RequestDiagnosticsMessage, ResponseDiagnosticsMessage, - UnidiciTimingInfo, + UndiciTimingInfo, } from './HttpClient.js'; import { HttpAgent, @@ -51,7 +50,7 @@ export type FetchDiagnosticsMessage = { export type FetchResponseDiagnosticsMessage = { fetch: FetchMeta; - timingInfo?: UnidiciTimingInfo; + timingInfo?: UndiciTimingInfo; response?: Response; error?: Error; }; @@ -236,8 +235,8 @@ export class FetchFactory { throw e; } - // get unidici internal response - const state = Reflect.get(res!, undiciFetchSymbols.kState) as Dispatcher.ResponseData; + // get undici internal response + const state = getResponseState(res!); updateSocketInfo(socketInfo, internalOpaque /* , rawError */); urllibResponse.headers = convertHeader(res!.headers); @@ -250,7 +249,7 @@ export class FetchFactory { channels.fetchResponse.publish({ fetch: fetchMeta, - timingInfo: (state as any).timingInfo, + timingInfo: state.timingInfo, response: res!, } as FetchResponseDiagnosticsMessage); channels.response.publish({