fix(telegram): preserve HTTP proxy env in global dispatcher workaround (#29940)

* fix(telegram): preserve HTTP proxy env in global dispatcher workaround

* telegram: document request-scoped proxy dispatcher constraint

* telegram: assert proxy path never mutates global dispatcher

* changelog: credit telegram proxy env regression fix

---------

Co-authored-by: Rylen Anil <rylen.anil@gmail.com>
This commit is contained in:
Vincent Koc
2026-03-01 13:21:01 -08:00
committed by GitHub
parent e7cafed424
commit e6049345db
5 changed files with 24 additions and 17 deletions

View File

@@ -88,6 +88,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/Startup (Raspberry Pi + small hosts): speed up startup by avoiding unnecessary plugin preload on fast routes, adding root `--version` fast-path bootstrap bypass, parallelizing status JSON/non-JSON scans where safe, and enabling Node compile cache at startup with env override compatibility (`NODE_COMPILE_CACHE`, `NODE_DISABLE_COMPILE_CACHE`). (#5871) Thanks @BookCatKid and @vincentkoc for raising startup reports, and @lupuletic for related startup work in #27973.
- Telegram/Outbound API proxy env: keep the Node 22 `autoSelectFamily` global-dispatcher workaround while restoring env-proxy support by using `EnvHttpProxyAgent` so `HTTP_PROXY`/`HTTPS_PROXY` continue to apply to outbound requests. (#26207) Thanks @qsysbio-cjw for reporting and @rylena and @vincentkoc for work.
- Docs/Slack manifest scopes: add missing DM/group-DM bot scopes (`im:read`, `im:write`, `mpim:read`, `mpim:write`) to the Slack app manifest example so DM setup guidance is complete. (#29999) Thanks @JcMinarro.
- Slack/Onboarding token help: update setup text to include the “From manifest” app-creation path and current install wording for obtaining the `xoxb-` bot token. (#30846) Thanks @yzhong52.
- Slack/Bot attachment-only messages: when `allowBots: true`, bot messages with empty `text` now include non-forwarded attachment `text`/`fallback` content so webhook alerts are not silently dropped. (#27616)

View File

@@ -5,8 +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 AgentCtor = vi.hoisted(() =>
vi.fn(function MockAgent(this: { options: unknown }, options: unknown) {
const EnvHttpProxyAgentCtor = vi.hoisted(() =>
vi.fn(function MockEnvHttpProxyAgent(this: { options: unknown }, options: unknown) {
this.options = options;
}),
);
@@ -28,7 +28,7 @@ vi.mock("node:dns", async () => {
});
vi.mock("undici", () => ({
Agent: AgentCtor,
EnvHttpProxyAgent: EnvHttpProxyAgentCtor,
setGlobalDispatcher,
}));
@@ -39,7 +39,7 @@ afterEach(() => {
setDefaultAutoSelectFamily.mockReset();
setDefaultResultOrder.mockReset();
setGlobalDispatcher.mockReset();
AgentCtor.mockClear();
EnvHttpProxyAgentCtor.mockClear();
vi.unstubAllEnvs();
vi.clearAllMocks();
if (originalFetch) {
@@ -147,12 +147,12 @@ describe("resolveTelegramFetch", () => {
expect(setDefaultResultOrder).toHaveBeenCalledTimes(2);
});
it("replaces global undici dispatcher with autoSelectFamily-enabled agent", async () => {
it("replaces global undici dispatcher with proxy-aware EnvHttpProxyAgent", async () => {
globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch;
resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } });
expect(setGlobalDispatcher).toHaveBeenCalledTimes(1);
expect(AgentCtor).toHaveBeenCalledWith({
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledWith({
connect: {
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 300,
@@ -174,13 +174,13 @@ describe("resolveTelegramFetch", () => {
resolveTelegramFetch(undefined, { network: { autoSelectFamily: false } });
expect(setGlobalDispatcher).toHaveBeenCalledTimes(2);
expect(AgentCtor).toHaveBeenNthCalledWith(1, {
expect(EnvHttpProxyAgentCtor).toHaveBeenNthCalledWith(1, {
connect: {
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 300,
},
});
expect(AgentCtor).toHaveBeenNthCalledWith(2, {
expect(EnvHttpProxyAgentCtor).toHaveBeenNthCalledWith(2, {
connect: {
autoSelectFamily: false,
autoSelectFamilyAttemptTimeout: 300,

View File

@@ -1,6 +1,6 @@
import * as dns from "node:dns";
import * as net from "node:net";
import { Agent, setGlobalDispatcher } from "undici";
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
import type { TelegramNetworkConfig } from "../config/types.telegram.js";
import { resolveFetch } from "../infra/fetch.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -46,7 +46,7 @@ function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void
) {
try {
setGlobalDispatcher(
new Agent({
new EnvHttpProxyAgent({
connect: {
autoSelectFamily: autoSelectDecision.value,
autoSelectFamilyAttemptTimeout: 300,

View File

@@ -1,8 +1,9 @@
import { describe, expect, it, vi } from "vitest";
const { ProxyAgent, undiciFetch, proxyAgentSpy, getLastAgent } = vi.hoisted(() => {
const mocks = vi.hoisted(() => {
const undiciFetch = vi.fn();
const proxyAgentSpy = vi.fn();
const setGlobalDispatcher = vi.fn();
class ProxyAgent {
static lastCreated: ProxyAgent | undefined;
proxyUrl: string;
@@ -17,13 +18,15 @@ const { ProxyAgent, undiciFetch, proxyAgentSpy, getLastAgent } = vi.hoisted(() =
ProxyAgent,
undiciFetch,
proxyAgentSpy,
setGlobalDispatcher,
getLastAgent: () => ProxyAgent.lastCreated,
};
});
vi.mock("undici", () => ({
ProxyAgent,
fetch: undiciFetch,
ProxyAgent: mocks.ProxyAgent,
fetch: mocks.undiciFetch,
setGlobalDispatcher: mocks.setGlobalDispatcher,
}));
import { makeProxyFetch } from "./proxy.js";
@@ -31,15 +34,16 @@ import { makeProxyFetch } from "./proxy.js";
describe("makeProxyFetch", () => {
it("uses undici fetch with ProxyAgent dispatcher", async () => {
const proxyUrl = "http://proxy.test:8080";
undiciFetch.mockResolvedValue({ ok: true });
mocks.undiciFetch.mockResolvedValue({ ok: true });
const proxyFetch = makeProxyFetch(proxyUrl);
await proxyFetch("https://api.telegram.org/bot123/getMe");
expect(proxyAgentSpy).toHaveBeenCalledWith(proxyUrl);
expect(undiciFetch).toHaveBeenCalledWith(
expect(mocks.proxyAgentSpy).toHaveBeenCalledWith(proxyUrl);
expect(mocks.undiciFetch).toHaveBeenCalledWith(
"https://api.telegram.org/bot123/getMe",
expect.objectContaining({ dispatcher: getLastAgent() }),
expect.objectContaining({ dispatcher: mocks.getLastAgent() }),
);
expect(mocks.setGlobalDispatcher).not.toHaveBeenCalled();
});
});

View File

@@ -4,6 +4,8 @@ export function makeProxyFetch(proxyUrl: string): typeof fetch {
const agent = new ProxyAgent(proxyUrl);
// undici's fetch is runtime-compatible with global fetch but the types diverge
// on stream/body internals. Single cast at the boundary keeps the rest type-safe.
// Keep proxy dispatching request-scoped. Replacing the global dispatcher breaks
// env-driven HTTP(S)_PROXY behavior for unrelated outbound requests.
const fetcher = ((input: RequestInfo | URL, init?: RequestInit) =>
undiciFetch(input as string | URL, {
...(init as Record<string, unknown>),