fix(telegram): refresh global undici dispatcher for autoSelectFamily (#25682)

Land PR #25682 from @lairtonlelis after maintainer rework:
track dispatcher updates when network decision changes to avoid stale global fetch behavior.

Co-authored-by: Ailton <lairton@telnyx.com>
This commit is contained in:
Peter Steinberger
2026-02-25 01:16:03 +00:00
parent bd213cf2ad
commit 0078070680
3 changed files with 85 additions and 0 deletions

View File

@@ -4,6 +4,12 @@ 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 AgentCtor = vi.hoisted(() =>
vi.fn(function MockAgent(this: { options: unknown }, options: unknown) {
this.options = options;
}),
);
vi.mock("node:net", async () => {
const actual = await vi.importActual<typeof import("node:net")>("node:net");
@@ -21,12 +27,19 @@ vi.mock("node:dns", async () => {
};
});
vi.mock("undici", () => ({
Agent: AgentCtor,
setGlobalDispatcher,
}));
const originalFetch = globalThis.fetch;
afterEach(() => {
resetTelegramFetchStateForTests();
setDefaultAutoSelectFamily.mockReset();
setDefaultResultOrder.mockReset();
setGlobalDispatcher.mockReset();
AgentCtor.mockClear();
vi.unstubAllEnvs();
vi.clearAllMocks();
if (originalFetch) {
@@ -133,4 +146,45 @@ describe("resolveTelegramFetch", () => {
expect(setDefaultResultOrder).toHaveBeenCalledTimes(2);
});
it("replaces global undici dispatcher with autoSelectFamily-enabled agent", async () => {
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
expect(setGlobalDispatcher).toHaveBeenCalledTimes(1);
expect(AgentCtor).toHaveBeenCalledWith({
connect: {
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 300,
},
});
});
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 } });
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
expect(setGlobalDispatcher).toHaveBeenCalledTimes(1);
});
it("updates global dispatcher when autoSelectFamily decision changes", async () => {
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
resolveTelegramFetch(undefined, { network: { autoSelectFamily: false } });
expect(setGlobalDispatcher).toHaveBeenCalledTimes(2);
expect(AgentCtor).toHaveBeenNthCalledWith(1, {
connect: {
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 300,
},
});
expect(AgentCtor).toHaveBeenNthCalledWith(2, {
connect: {
autoSelectFamily: false,
autoSelectFamilyAttemptTimeout: 300,
},
});
});
});

View File

@@ -1,5 +1,6 @@
import * as dns from "node:dns";
import * as net from "node:net";
import { Agent, setGlobalDispatcher } from "undici";
import type { TelegramNetworkConfig } from "../config/types.telegram.js";
import { resolveFetch } from "../infra/fetch.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -10,6 +11,7 @@ import {
let appliedAutoSelectFamily: boolean | null = null;
let appliedDnsResultOrder: string | null = null;
let appliedGlobalDispatcherAutoSelectFamily: boolean | null = null;
const log = createSubsystemLogger("telegram/network");
// Node 22 workaround: enable autoSelectFamily to allow IPv4 fallback on broken IPv6 networks.
@@ -31,6 +33,33 @@ function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void
}
}
// Node 22's built-in globalThis.fetch uses undici's internal Agent whose
// connect options are frozen at construction time. Calling
// net.setDefaultAutoSelectFamily() after that agent is created has no
// effect on it. Replace the global dispatcher with one that carries the
// current autoSelectFamily setting so subsequent globalThis.fetch calls
// inherit the same decision.
// See: https://github.com/openclaw/openclaw/issues/25676
if (
autoSelectDecision.value !== null &&
autoSelectDecision.value !== appliedGlobalDispatcherAutoSelectFamily
) {
try {
setGlobalDispatcher(
new Agent({
connect: {
autoSelectFamily: autoSelectDecision.value,
autoSelectFamilyAttemptTimeout: 300,
},
}),
);
appliedGlobalDispatcherAutoSelectFamily = autoSelectDecision.value;
log.info(`global undici dispatcher autoSelectFamily=${autoSelectDecision.value}`);
} catch {
// ignore if setGlobalDispatcher is unavailable
}
}
// Apply DNS result order workaround for IPv4/IPv6 issues.
// Some APIs (including Telegram) may fail with IPv6 on certain networks.
// See: https://github.com/openclaw/openclaw/issues/5311
@@ -68,4 +97,5 @@ export function resolveTelegramFetch(
export function resetTelegramFetchStateForTests(): void {
appliedAutoSelectFamily = null;
appliedDnsResultOrder = null;
appliedGlobalDispatcherAutoSelectFamily = null;
}