|
6 | 6 | // |
7 | 7 | // Loopback targets (localhost/127.0.0.1/::1) always bypass the proxy so the |
8 | 8 | // default managed backend (`http://127.0.0.1:10001`) is never proxied. |
9 | | -import { ProxyAgent, Socks5ProxyAgent, fetch as undiciFetch } from 'undici'; |
| 9 | +// |
| 10 | +// undici is loaded lazily (dynamic import) so hosts on older Node keep |
| 11 | +// running direct-only: only actual proxy engagement needs it, and a load |
| 12 | +// failure fails open to direct with a warning. |
10 | 13 | import type { Dispatcher } from 'undici'; |
11 | 14 |
|
12 | 15 | export type ProxyStrategy = 'failover-rr' | 'round-robin' | 'random'; |
@@ -152,14 +155,39 @@ export function createProxyPool(options: ProxyPoolOptions = {}): UpstreamProxyPo |
152 | 155 | let engagedUntil = 0; |
153 | 156 | let current: string | null = null; |
154 | 157 |
|
155 | | - const getDispatcher = (proxyUrl: string): Dispatcher => { |
| 158 | + // Cached dynamic import: resolved at most once; a rejection (e.g. Node too |
| 159 | + // old for undici) is memorized so every proxied call fails open to direct. |
| 160 | + let undiciModule: { |
| 161 | + ProxyAgent: new (url: string) => Dispatcher; |
| 162 | + Socks5ProxyAgent: new (url: string) => Dispatcher; |
| 163 | + fetch: (input: unknown, init?: unknown) => Promise<unknown>; |
| 164 | + } | null = null; |
| 165 | + let undiciFailed = false; |
| 166 | + const loadUndici = async (): Promise<typeof undiciModule> => { |
| 167 | + if (undiciModule || undiciFailed) return undiciModule; |
| 168 | + try { |
| 169 | + const mod = (await import('undici')) as unknown as NonNullable<typeof undiciModule>; |
| 170 | + undiciModule = mod; |
| 171 | + return mod; |
| 172 | + } catch (err) { |
| 173 | + undiciFailed = true; |
| 174 | + logDebug('Upstream proxy runtime unavailable, staying direct', { |
| 175 | + error: err instanceof Error ? err.message : String(err), |
| 176 | + }); |
| 177 | + return null; |
| 178 | + } |
| 179 | + }; |
| 180 | + |
| 181 | + const getDispatcher = async (proxyUrl: string): Promise<Dispatcher | null> => { |
156 | 182 | const hit = dispatchers.get(proxyUrl); |
157 | 183 | if (hit) return hit; |
| 184 | + const mod = await loadUndici(); |
| 185 | + if (!mod) return null; |
158 | 186 | const protocol = new URL(proxyUrl).protocol; |
159 | 187 | const agent: Dispatcher = |
160 | 188 | protocol === 'socks:' || protocol === 'socks5:' || protocol === 'socks5h:' |
161 | | - ? ((new Socks5ProxyAgent(proxyUrl) as unknown) as Dispatcher) |
162 | | - : ((new ProxyAgent(proxyUrl) as unknown) as Dispatcher); |
| 189 | + ? new mod.Socks5ProxyAgent(proxyUrl) |
| 190 | + : new mod.ProxyAgent(proxyUrl); |
163 | 191 | dispatchers.set(proxyUrl, agent); |
164 | 192 | return agent; |
165 | 193 | }; |
@@ -224,10 +252,13 @@ export function createProxyPool(options: ProxyPoolOptions = {}): UpstreamProxyPo |
224 | 252 | if (!proxyUrl) return direct(); |
225 | 253 | current = proxyUrl; |
226 | 254 | try { |
227 | | - return await (undiciFetch as (i: unknown, o?: unknown) => Promise<unknown>)(input as never, { |
| 255 | + const mod = await loadUndici(); |
| 256 | + const dispatcher = proxyUrl ? await getDispatcher(proxyUrl) : null; |
| 257 | + if (!mod || !dispatcher) return direct(); |
| 258 | + return await mod.fetch(input as never, { |
228 | 259 | ...((init as Record<string, unknown>) ?? {}), |
229 | | - dispatcher: getDispatcher(proxyUrl), |
230 | | - }); |
| 260 | + dispatcher, |
| 261 | + } as never); |
231 | 262 | } catch (err) { |
232 | 263 | // Transport failure through this proxy: cool it, fail open to direct |
233 | 264 | // for this call (quota errors are handled by callers, not here). |
|
0 commit comments