fix(telegram): honor managed proxy env

This commit is contained in:
Peter Steinberger
2026-04-29 12:18:39 +01:00
parent 89cd2b6362
commit dc9f1b8525
7 changed files with 120 additions and 23 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/auth: keep OAuth auth profiles inherited from the main agent read-through instead of copying refresh tokens into secondary agents, and refresh Codex app-server tokens against the owning store so multi-agent swarms avoid reused refresh-token failures. Fixes #74055. Thanks @ClarityInvest.
- Channels/Telegram: honor `ALL_PROXY` / `all_proxy` and service-level `OPENCLAW_PROXY_URL` when constructing the HTTP/1-only Telegram Bot API transport, so Windows and service installs that rely on those proxy settings no longer fall back to direct egress. Fixes #74014; refs #74086. Thanks @SymbolStar.
- ACP/commands: accept forwarded ACP timeout config controls in the OpenClaw bridge, treat unsupported discard-close controls as recoverable cleanup, and restore native `/verbose full` plus no-arg status behavior, so Discord command menus and nested ACP turns no longer fail on supported session controls. Thanks @vincentkoc.
- TUI/status: clear stale `streaming` footer state when a final event arrives after the active run was already cleared and no tracked runs remain, while preserving concurrent-run ownership and inactive local `/btw` terminal handling. Fixes #64825; carries forward #64842, #64843, #64847, and #64862. Thanks @briandevans and @Yanhu007.
- Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.

View File

@@ -867,6 +867,8 @@ Per-account, per-group, and per-topic overrides are supported (same inheritance
- If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors.
- If logs include `Polling stall detected`, OpenClaw restarts polling and rebuilds the Telegram transport after 120 seconds without completed long-poll liveness by default.
- Increase `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues between the host and `api.telegram.org`.
- Telegram also honors process proxy env for Bot API transport, including `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and their lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`.
- If the OpenClaw managed proxy is configured through `OPENCLAW_PROXY_URL` for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too.
- On VPS hosts with unstable direct egress/TLS, route Telegram API calls through `channels.telegram.proxy`:
```yaml

View File

@@ -438,7 +438,7 @@ releases.
| `plugin-sdk/collection-runtime` | Bounded cache helpers | `pruneMapToMaxSize` |
| `plugin-sdk/diagnostic-runtime` | Diagnostic gating helpers | `isDiagnosticFlagEnabled`, `isDiagnosticsEnabled` |
| `plugin-sdk/error-runtime` | Error formatting helpers | `formatUncaughtError`, `isApprovalNotFoundError`, error graph helpers |
| `plugin-sdk/fetch-runtime` | Wrapped fetch/proxy helpers | `resolveFetch`, proxy helpers |
| `plugin-sdk/fetch-runtime` | Wrapped fetch/proxy helpers | `resolveFetch`, proxy helpers, EnvHttpProxyAgent option helpers |
| `plugin-sdk/host-runtime` | Host normalization helpers | `normalizeHostname`, `normalizeScpRemoteHost` |
| `plugin-sdk/retry-runtime` | Retry helpers | `RetryConfig`, `retryAsync`, policy runners |
| `plugin-sdk/allow-from` | Allowlist formatting | `formatAllowFromLowercase` |

View File

@@ -241,7 +241,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/collection-runtime` | Small bounded cache helpers |
| `plugin-sdk/diagnostic-runtime` | Diagnostic flag, event, and trace-context helpers |
| `plugin-sdk/error-runtime` | Error graph, formatting, shared error classification helpers, `isApprovalNotFoundError` |
| `plugin-sdk/fetch-runtime` | Wrapped fetch, proxy, and pinned lookup helpers |
| `plugin-sdk/fetch-runtime` | Wrapped fetch, proxy, EnvHttpProxyAgent option, and pinned lookup helpers |
| `plugin-sdk/runtime-fetch` | Dispatcher-aware runtime fetch without proxy/guarded-fetch imports |
| `plugin-sdk/response-limit-runtime` | Bounded response-body reader without the broad media runtime surface |
| `plugin-sdk/session-binding-runtime` | Current conversation binding state without configured binding routing or pairing stores |

View File

@@ -123,6 +123,7 @@ beforeEach(() => {
"https_proxy",
"NO_PROXY",
"no_proxy",
"OPENCLAW_PROXY_URL",
]) {
vi.stubEnv(key, "");
}
@@ -386,6 +387,11 @@ describe("resolveTelegramFetch", () => {
await resolved("https://api.telegram.org/botx/getMe");
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledTimes(1);
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledWith(
expect.objectContaining({
httpsProxy: "http://127.0.0.1:7890",
}),
);
expect(AgentCtor).not.toHaveBeenCalled();
const dispatcher = getDispatcherFromUndiciCall(1);
@@ -421,6 +427,80 @@ describe("resolveTelegramFetch", () => {
);
});
it("uses OPENCLAW_PROXY_URL as a Telegram explicit proxy when proxy env is absent", async () => {
vi.stubEnv("OPENCLAW_PROXY_URL", "http://127.0.0.1:7788");
undiciFetch.mockResolvedValue({ ok: true } as Response);
const transport = resolveTelegramTransport(undefined, {
network: {
autoSelectFamily: false,
dnsResultOrder: "ipv4first",
},
});
await transport.fetch("https://api.telegram.org/botTOKEN/getMe");
expect(ProxyAgentCtor).toHaveBeenCalledTimes(1);
expect(ProxyAgentCtor).toHaveBeenCalledWith(
expect.objectContaining({
allowH2: false,
uri: "http://127.0.0.1:7788",
requestTls: expect.objectContaining({
autoSelectFamily: false,
}),
}),
);
expect(EnvHttpProxyAgentCtor).not.toHaveBeenCalled();
expect(AgentCtor).not.toHaveBeenCalled();
expect(transport.dispatcherAttempts?.[0]?.dispatcherPolicy).toEqual(
expect.objectContaining({
mode: "explicit-proxy",
proxyUrl: "http://127.0.0.1:7788",
}),
);
});
it("preserves caller-provided custom fetch when OPENCLAW_PROXY_URL is present", async () => {
vi.stubEnv("OPENCLAW_PROXY_URL", "http://127.0.0.1:7788");
const proxyFetch = vi.fn(async () => ({ ok: true }) as Response) as unknown as typeof fetch;
const transport = resolveTelegramTransport(proxyFetch, {
network: {
autoSelectFamily: false,
dnsResultOrder: "ipv4first",
},
});
await transport.fetch("https://api.telegram.org/botTOKEN/getMe");
expect(proxyFetch).toHaveBeenCalledTimes(1);
expect(undiciFetch).not.toHaveBeenCalled();
expect(ProxyAgentCtor).not.toHaveBeenCalled();
expect(EnvHttpProxyAgentCtor).not.toHaveBeenCalled();
expect(AgentCtor).not.toHaveBeenCalled();
expect(transport.sourceFetch).not.toBe(undiciFetch);
expect(transport.dispatcherAttempts).toBeUndefined();
});
it("prefers standard proxy env over OPENCLAW_PROXY_URL for Telegram", async () => {
vi.stubEnv("OPENCLAW_PROXY_URL", "http://127.0.0.1:7788");
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
undiciFetch.mockResolvedValue({ ok: true } as Response);
const resolved = resolveTelegramFetchOrThrow(undefined, {
network: {
autoSelectFamily: false,
dnsResultOrder: "ipv4first",
},
});
await resolved("https://api.telegram.org/botx/getMe");
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledTimes(1);
expect(ProxyAgentCtor).not.toHaveBeenCalled();
expect(AgentCtor).not.toHaveBeenCalled();
});
it("pins env-proxy transport policy onto proxyTls for proxied HTTPS requests", async () => {
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
undiciFetch.mockResolvedValue({ ok: true } as Response);
@@ -572,12 +652,10 @@ describe("resolveTelegramFetch", () => {
});
});
it("treats ALL_PROXY-only env as direct transport and arms sticky IPv4 fallback", async () => {
vi.stubEnv("ALL_PROXY", "socks5://127.0.0.1:1080");
undiciFetch
.mockRejectedValueOnce(buildFetchFallbackError("EHOSTUNREACH"))
.mockResolvedValueOnce({ ok: true } as Response)
.mockResolvedValueOnce({ ok: true } as Response);
it("uses ALL_PROXY env as EnvHttpProxyAgent transport", async () => {
vi.stubEnv("ALL_PROXY", "http://127.0.0.1:7891");
vi.stubEnv("all_proxy", "http://127.0.0.1:7891");
undiciFetch.mockResolvedValue({ ok: true } as Response);
const transport = resolveTelegramTransport(undefined, {
network: {
@@ -588,19 +666,20 @@ describe("resolveTelegramFetch", () => {
const resolved = transport.fetch;
await resolved("https://api.telegram.org/botx/sendMessage");
await resolved("https://api.telegram.org/botx/sendChatAction");
expect(EnvHttpProxyAgentCtor).not.toHaveBeenCalled();
expect(AgentCtor).toHaveBeenCalledTimes(2);
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledTimes(1);
expect(EnvHttpProxyAgentCtor).toHaveBeenCalledWith(
expect.objectContaining({
allowH2: false,
httpProxy: "http://127.0.0.1:7891",
httpsProxy: "http://127.0.0.1:7891",
}),
);
expect(AgentCtor).not.toHaveBeenCalled();
expectPinnedIpv4ConnectDispatcher({
firstCall: 1,
pinnedCall: 2,
followupCall: 3,
});
expect(transport.dispatcherAttempts?.[0]?.dispatcherPolicy).toEqual(
expect.objectContaining({
mode: "direct",
mode: "env-proxy",
}),
);
});

View File

@@ -4,7 +4,8 @@ import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-types";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
createPinnedLookup,
hasEnvHttpProxyConfigured,
hasEnvHttpProxyAgentConfigured,
resolveEnvHttpProxyAgentOptions,
resolveFetch,
type PinnedDispatcherPolicy,
} from "openclaw/plugin-sdk/fetch-runtime";
@@ -225,7 +226,14 @@ function shouldBypassEnvProxyForTelegramApi(env: NodeJS.ProcessEnv = process.env
}
function hasEnvHttpProxyForTelegramApi(env: NodeJS.ProcessEnv = process.env): boolean {
return hasEnvHttpProxyConfigured("https", env);
return hasEnvHttpProxyAgentConfigured(env);
}
function resolveOpenClawProxyUrlForTelegram(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
const proxyUrl = env.OPENCLAW_PROXY_URL?.trim();
return proxyUrl ? proxyUrl : undefined;
}
function resolveTelegramDispatcherPolicy(params: {
@@ -325,6 +333,7 @@ function createTelegramDispatcher(policy: PinnedDispatcherPolicy): {
const proxyTlsOptions = withPinnedLookup(policy.proxyTls, policy.pinnedHostname);
const proxyOptions = {
...poolOptions,
...resolveEnvHttpProxyAgentOptions(),
...(connectOptions ? { connect: connectOptions } : {}),
...(proxyTlsOptions ? { proxyTls: proxyTlsOptions } : {}),
} satisfies ConstructorParameters<typeof EnvHttpProxyAgent>[0];
@@ -580,8 +589,12 @@ export function resolveTelegramTransport(
const explicitProxyUrl = effectiveProxyFetch
? getProxyUrlFromFetch(effectiveProxyFetch)
: undefined;
const hasEnvProxy = !explicitProxyUrl && hasEnvHttpProxyForTelegramApi();
const managedProxyUrl =
!effectiveProxyFetch && !hasEnvProxy ? resolveOpenClawProxyUrlForTelegram() : undefined;
const resolvedExplicitProxyUrl = explicitProxyUrl ?? managedProxyUrl;
const undiciSourceFetch = resolveWrappedFetch(undiciFetch as unknown as typeof fetch);
const sourceFetch = explicitProxyUrl
const sourceFetch = resolvedExplicitProxyUrl
? undiciSourceFetch
: effectiveProxyFetch
? resolveWrappedFetch(effectiveProxyFetch)
@@ -592,13 +605,13 @@ export function resolveTelegramTransport(
return { fetch: sourceFetch, sourceFetch, close: async () => {} };
}
const useEnvProxy = !explicitProxyUrl && hasEnvHttpProxyForTelegramApi();
const useEnvProxy = !resolvedExplicitProxyUrl && hasEnvProxy;
const defaultDispatcherResolution = resolveTelegramDispatcherPolicy({
autoSelectFamily: autoSelectDecision.value,
dnsResultOrder,
useEnvProxy,
forceIpv4: false,
proxyUrl: explicitProxyUrl,
proxyUrl: resolvedExplicitProxyUrl,
});
const defaultDispatcher = createTelegramDispatcher(defaultDispatcherResolution.policy);
const shouldBypassEnvProxy = shouldBypassEnvProxyForTelegramApi();
@@ -611,7 +624,7 @@ export function resolveTelegramTransport(
dnsResultOrder: "ipv4first",
useEnvProxy: defaultDispatcher.mode === "env-proxy",
forceIpv4: true,
proxyUrl: explicitProxyUrl,
proxyUrl: resolvedExplicitProxyUrl,
}).policy
: undefined;
const ownedDispatchers = new Set<TelegramDispatcher>();

View File

@@ -4,6 +4,8 @@ export { resolveFetch, wrapFetchWithAbortSignal } from "../infra/fetch.js";
export { withTrustedEnvProxyGuardedFetchMode } from "../infra/net/fetch-guard.ts";
export {
hasEnvHttpProxyConfigured,
hasEnvHttpProxyAgentConfigured,
resolveEnvHttpProxyAgentOptions,
resolveEnvHttpProxyUrl,
shouldUseEnvHttpProxyForUrl,
} from "../infra/net/proxy-env.js";