From 8ecf9fb8c3ced1b014efc2de2af1acb5aafb11cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:23:03 +0000 Subject: [PATCH 1/2] chore(deps): update dependency undici to v8.9.0 --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f8dbc2d..0ab3f4f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,7 +38,7 @@ importers: version: 4.41.0 undici: specifier: ^8.4.1 - version: 8.5.0 + version: 8.9.0 ylru: specifier: ^2.0.0 version: 2.0.0 @@ -1775,8 +1775,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@8.5.0: - resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} engines: {node: '>=22.19.0'} unicode-emoji-modifier-base@1.0.0: @@ -3226,7 +3226,7 @@ snapshots: undici-types@6.21.0: {} - undici@8.5.0: {} + undici@8.9.0: {} unicode-emoji-modifier-base@1.0.0: {} From 352dabb6fb53bd8e7404db5be4130cd460fc5b36 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 31 Jul 2026 23:51:27 +0800 Subject: [PATCH 2/2] fix: work around undici 8.8+ idle socket keepalive deadlock undici 8.8.0 defers writing a request to an idle keep-alive socket behind an unref'ed setImmediate (nodejs/undici#5600), which cannot wake an idle event loop, so sequential keepalive requests can hang. Schedule a ref'ed no-op immediate at dispatch time so the deferred write runs. Remove once undici ships nodejs/undici#5606. --- src/HttpClient.ts | 13 ++++++++++++- src/fetch.ts | 4 +++- src/utils.ts | 13 +++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/HttpClient.ts b/src/HttpClient.ts index 3dd980af..7f1cc4ce 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -33,7 +33,15 @@ import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js'; import type { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request.js'; import type { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response.js'; import symbols from './symbols.js'; -import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable, updateSocketInfo } from './utils.js'; +import { + parseJSON, + digestAuthHeader, + globalId, + performanceTime, + isReadable, + updateSocketInfo, + wakeupEventLoopOnDispatch, +} from './utils.js'; type Exists = T extends undefined ? never : T; type UndiciRequestOption = Exists[1]>; @@ -650,6 +658,8 @@ export class HttpClient extends EventEmitter { this.emit('request', reqMeta); } + // wake the event loop so undici's idle socket validation immediate runs, see utils.ts + wakeupEventLoopOnDispatch(); let response = await undiciRequest(requestUrl, requestOptions as UndiciRequestOption); if ( response.statusCode === 401 && @@ -677,6 +687,7 @@ export class HttpClient extends EventEmitter { } // Ensure the previous response is consumed as we re-use the same variable await response.body.arrayBuffer(); + wakeupEventLoopOnDispatch(); response = await undiciRequest(requestUrl, requestOptions as UndiciRequestOption); } } diff --git a/src/fetch.ts b/src/fetch.ts index 30e4691e..220f1d8f 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -25,7 +25,7 @@ import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js'; import type { FetchMeta, HttpMethod, RequestMeta } from './Request.js'; import type { RawResponseWithMeta, SocketInfo } from './Response.js'; import symbols from './symbols.js'; -import { convertHeader, globalId, performanceTime, updateSocketInfo } from './utils.js'; +import { convertHeader, globalId, performanceTime, updateSocketInfo, wakeupEventLoopOnDispatch } from './utils.js'; const debug = debuglog('urllib/fetch'); @@ -197,6 +197,8 @@ export class FetchFactory { } as any as RawResponseWithMeta; try { await this.#opaqueLocalStorage.run(internalOpaque, async () => { + // wake the event loop so undici's idle socket validation immediate runs, see utils.ts + wakeupEventLoopOnDispatch(); res = await UndiciFetch(request); }); } catch (e: any) { diff --git a/src/utils.ts b/src/utils.ts index a2cdc24f..7b6a740f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -147,6 +147,19 @@ export function performanceTime(startTime: number, now?: number): number { return Math.floor(((now ?? performance.now()) - startTime) * 1000) / 1000; } +const noop = () => {}; + +// undici@8.8.0 defers writing a request to an idle keep-alive socket behind an +// unref'ed setImmediate ("idle socket validation", nodejs/undici#5600). An +// unref'ed immediate cannot wake an idle event loop, so when nothing else wakes +// it the deferred write never happens and the request hangs. Scheduling one +// ref'ed no-op immediate at dispatch time forces the next check phase to run, +// which also runs undici's validation immediate. +// Remove once undici ships nodejs/undici#5606 and the dependency is bumped past it. +export function wakeupEventLoopOnDispatch(): void { + setImmediate(noop); +} + export function isReadable(stream: any): boolean { if (typeof Readable.isReadable === 'function') return Readable.isReadable(stream); // patch from node