diff --git a/src/telegram/fetch.test.ts b/src/telegram/fetch.test.ts index f41631a2d6c..ca040e56839 100644 --- a/src/telegram/fetch.test.ts +++ b/src/telegram/fetch.test.ts @@ -5,6 +5,8 @@ import { resetTelegramFetchStateForTests, resolveTelegramFetch } from "./fetch.j const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn()); const setDefaultResultOrder = vi.hoisted(() => vi.fn()); const setGlobalDispatcher = vi.hoisted(() => vi.fn()); +const getGlobalDispatcherState = vi.hoisted(() => ({ value: undefined as unknown })); +const getGlobalDispatcher = vi.hoisted(() => vi.fn(() => getGlobalDispatcherState.value)); const EnvHttpProxyAgentCtor = vi.hoisted(() => vi.fn(function MockEnvHttpProxyAgent(this: { options: unknown }, options: unknown) { this.options = options; @@ -29,6 +31,7 @@ vi.mock("node:dns", async () => { vi.mock("undici", () => ({ EnvHttpProxyAgent: EnvHttpProxyAgentCtor, + getGlobalDispatcher, setGlobalDispatcher, })); @@ -39,6 +42,8 @@ afterEach(() => { setDefaultAutoSelectFamily.mockReset(); setDefaultResultOrder.mockReset(); setGlobalDispatcher.mockReset(); + getGlobalDispatcher.mockClear(); + getGlobalDispatcherState.value = undefined; EnvHttpProxyAgentCtor.mockClear(); vi.unstubAllEnvs(); vi.clearAllMocks(); @@ -160,6 +165,31 @@ describe("resolveTelegramFetch", () => { }); }); + it("keeps an existing proxy-like global dispatcher", async () => { + getGlobalDispatcherState.value = { + constructor: { name: "ProxyAgent" }, + }; + globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; + + resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); + + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(EnvHttpProxyAgentCtor).not.toHaveBeenCalled(); + }); + + it("updates proxy-like dispatcher when proxy env is configured", async () => { + vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890"); + getGlobalDispatcherState.value = { + constructor: { name: "ProxyAgent" }, + }; + globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; + + resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); + + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + expect(EnvHttpProxyAgentCtor).toHaveBeenCalledTimes(1); + }); + it("sets global dispatcher only once across repeated equal decisions", async () => { globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts index 8755003ea98..569d2c33828 100644 --- a/src/telegram/fetch.ts +++ b/src/telegram/fetch.ts @@ -1,6 +1,6 @@ import * as dns from "node:dns"; import * as net from "node:net"; -import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; +import { EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici"; import type { TelegramNetworkConfig } from "../config/types.telegram.js"; import { resolveFetch } from "../infra/fetch.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -13,6 +13,29 @@ let appliedAutoSelectFamily: boolean | null = null; let appliedDnsResultOrder: string | null = null; let appliedGlobalDispatcherAutoSelectFamily: boolean | null = null; const log = createSubsystemLogger("telegram/network"); +const PROXY_ENV_KEYS = [ + "HTTPS_PROXY", + "HTTP_PROXY", + "ALL_PROXY", + "https_proxy", + "http_proxy", + "all_proxy", +] as const; + +function hasProxyEnvConfigured(): boolean { + for (const key of PROXY_ENV_KEYS) { + const value = process.env[key]; + if (typeof value === "string" && value.trim().length > 0) { + return true; + } + } + return false; +} + +function isProxyLikeDispatcher(dispatcher: unknown): boolean { + const ctorName = (dispatcher as { constructor?: { name?: string } })?.constructor?.name; + return typeof ctorName === "string" && ctorName.includes("ProxyAgent"); +} // Node 22 workaround: enable autoSelectFamily to allow IPv4 fallback on broken IPv6 networks. // Many networks have IPv6 configured but not routed, causing "Network is unreachable" errors. @@ -44,19 +67,24 @@ function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void autoSelectDecision.value !== null && autoSelectDecision.value !== appliedGlobalDispatcherAutoSelectFamily ) { - try { - setGlobalDispatcher( - new EnvHttpProxyAgent({ - connect: { - autoSelectFamily: autoSelectDecision.value, - autoSelectFamilyAttemptTimeout: 300, - }, - }), - ); - appliedGlobalDispatcherAutoSelectFamily = autoSelectDecision.value; - log.info(`global undici dispatcher autoSelectFamily=${autoSelectDecision.value}`); - } catch { - // ignore if setGlobalDispatcher is unavailable + const existingGlobalDispatcher = getGlobalDispatcher(); + const shouldPreserveExistingProxy = + isProxyLikeDispatcher(existingGlobalDispatcher) && !hasProxyEnvConfigured(); + if (!shouldPreserveExistingProxy) { + try { + setGlobalDispatcher( + new EnvHttpProxyAgent({ + connect: { + autoSelectFamily: autoSelectDecision.value, + autoSelectFamilyAttemptTimeout: 300, + }, + }), + ); + appliedGlobalDispatcherAutoSelectFamily = autoSelectDecision.value; + log.info(`global undici dispatcher autoSelectFamily=${autoSelectDecision.value}`); + } catch { + // ignore if setGlobalDispatcher is unavailable + } } }