From 2465217b238ac50413fc5051dd1884504050970d Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 6 May 2026 16:50:42 +0530 Subject: [PATCH] fix(net): bound guarded fetch dispatcher cleanup --- src/infra/net/ssrf.ts | 53 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index 862901574b2..79fec3cc752 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -27,6 +27,7 @@ type LookupCallback = ( ) => void; type LookupResult = LookupAddress | LookupAddress[]; +const DISPATCHER_CLOSE_TIMEOUT_MS = 100; export class SsrFBlockedError extends Error { constructor(message: string) { @@ -551,19 +552,55 @@ export function createPinnedDispatcher( ); } +type ClosableDispatcher = { + close?: () => Promise | void; + destroy?: () => void; +}; + +function destroyDispatcher(candidate: ClosableDispatcher): void { + try { + candidate.destroy?.(); + } catch { + // ignore dispatcher cleanup errors + } +} + +async function waitForDispatcherClose(candidate: ClosableDispatcher): Promise { + const close = candidate.close; + if (typeof close !== "function") { + destroyDispatcher(candidate); + return; + } + let timeout: ReturnType | undefined; + try { + await Promise.race([ + Promise.resolve(close.call(candidate)), + new Promise((resolve) => { + timeout = setTimeout(() => { + timeout = undefined; + destroyDispatcher(candidate); + resolve(); + }, DISPATCHER_CLOSE_TIMEOUT_MS); + timeout.unref?.(); + }), + ]); + } catch (err) { + destroyDispatcher(candidate); + throw err; + } finally { + if (timeout) { + clearTimeout(timeout); + } + } +} + export async function closeDispatcher(dispatcher?: Dispatcher | null): Promise { if (!dispatcher) { return; } - const candidate = dispatcher as { close?: () => Promise | void; destroy?: () => void }; + const candidate = dispatcher as ClosableDispatcher; try { - if (typeof candidate.close === "function") { - await candidate.close(); - return; - } - if (typeof candidate.destroy === "function") { - candidate.destroy(); - } + await waitForDispatcherClose(candidate); } catch { // ignore dispatcher cleanup errors }