fix: quiet telegram ipv4 fallback noise

This commit is contained in:
Peter Steinberger
2026-05-02 05:39:23 +01:00
parent 3e02bc2f28
commit e873c1e1f8
6 changed files with 68 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana.
- Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw.
- Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc.
- ACP/Discord: suppress completion announce delivery for inline thread-bound ACP session runs, so Discord thread-bound ACP replies are not delivered twice. Fixes #60780. Thanks @solavrc.

View File

@@ -878,7 +878,7 @@ channels:
proxy: socks5://<user>:<password>@proxy-host:1080
```
- Node 22+ defaults to `autoSelectFamily=true` (except WSL2) and `dnsResultOrder=ipv4first`.
- Node 22+ defaults to `autoSelectFamily=true` (except WSL2). Telegram DNS result order honors `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`, then `channels.telegram.network.dnsResultOrder`, then the process default such as `NODE_OPTIONS=--dns-result-order=ipv4first`; if none applies, Node 22+ falls back to `ipv4first`.
- If your host is WSL2 or explicitly works better with IPv4-only behavior, force family selection:
```yaml

View File

@@ -2,9 +2,11 @@ import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const setDefaultResultOrder = vi.hoisted(() => vi.fn());
const getDefaultResultOrder = vi.hoisted(() => vi.fn(() => "ipv4first"));
const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn());
const loggerInfo = vi.hoisted(() => vi.fn());
const loggerDebug = vi.hoisted(() => vi.fn());
const loggerWarn = vi.hoisted(() => vi.fn());
const undiciFetch = vi.hoisted(() => vi.fn());
const setGlobalDispatcher = vi.hoisted(() => vi.fn());
@@ -46,6 +48,7 @@ vi.mock("node:dns", async () => {
const actual = await vi.importActual<typeof import("node:dns")>("node:dns");
return {
...actual,
getDefaultResultOrder,
setDefaultResultOrder,
};
});
@@ -70,12 +73,12 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
createSubsystemLogger: () => ({
info: loggerInfo,
debug: loggerDebug,
warn: vi.fn(),
warn: loggerWarn,
error: vi.fn(),
child: () => ({
info: loggerInfo,
debug: loggerDebug,
warn: vi.fn(),
warn: loggerWarn,
error: vi.fn(),
}),
}),
@@ -129,6 +132,9 @@ beforeEach(() => {
}
loggerInfo.mockReset();
loggerDebug.mockReset();
loggerWarn.mockReset();
getDefaultResultOrder.mockReset();
getDefaultResultOrder.mockReturnValue("ipv4first");
});
afterEach(() => {
@@ -368,9 +374,9 @@ describe("resolveTelegramFetch", () => {
resolveTelegramFetchOrThrow();
expect(loggerInfo).not.toHaveBeenCalledWith("autoSelectFamily=true (default-node22)");
expect(loggerInfo).not.toHaveBeenCalledWith("dnsResultOrder=ipv4first (default-node22)");
expect(loggerInfo).not.toHaveBeenCalledWith("dnsResultOrder=ipv4first (process-default)");
expect(loggerDebug).toHaveBeenCalledWith("autoSelectFamily=true (default-node22)");
expect(loggerDebug).toHaveBeenCalledWith("dnsResultOrder=ipv4first (default-node22)");
expect(loggerDebug).toHaveBeenCalledWith("dnsResultOrder=ipv4first (process-default)");
});
it("uses EnvHttpProxyAgent dispatcher when proxy env is configured", async () => {
@@ -813,6 +819,12 @@ describe("resolveTelegramFetch", () => {
autoSelectFamily: false,
}),
);
expect(loggerDebug).toHaveBeenCalledWith(
expect.stringContaining("fetch fallback: enabling sticky IPv4-only dispatcher"),
);
expect(loggerWarn).not.toHaveBeenCalledWith(
expect.stringContaining("fetch fallback: enabling sticky IPv4-only dispatcher"),
);
});
it("escalates from IPv4 fallback to pinned Telegram IP and keeps it sticky", async () => {
@@ -841,6 +853,9 @@ describe("resolveTelegramFetch", () => {
expect(secondDispatcher).not.toBe(thirdDispatcher);
expect(thirdDispatcher).toBe(fourthDispatcher);
expectPinnedFallbackIpDispatcher(3);
expect(loggerWarn).toHaveBeenCalledWith(
expect.stringContaining("fetch fallback: DNS-resolved IP unreachable"),
);
});
it("keeps the armed fallback sticky when all attempts fail", async () => {

View File

@@ -75,6 +75,7 @@ type TelegramDispatcherAttempt = {
type TelegramTransportAttempt = {
createDispatcher: () => TelegramDispatcher;
exportAttempt: TelegramDispatcherAttempt;
logLevel?: "debug" | "warn";
logMessage?: string;
};
@@ -518,6 +519,7 @@ function createTelegramTransportAttempts(params: {
return ipv4Dispatcher;
},
exportAttempt: { dispatcherPolicy: fallbackPolicy },
logLevel: "debug",
logMessage: "fetch fallback: enabling sticky IPv4-only dispatcher",
});
@@ -542,6 +544,7 @@ function createTelegramTransportAttempts(params: {
return fallbackIpDispatcher;
},
exportAttempt: { dispatcherPolicy: fallbackIpPolicy },
logLevel: "warn",
logMessage: "fetch fallback: DNS-resolved IP unreachable; trying alternative Telegram API IP",
});
@@ -643,7 +646,12 @@ export function resolveTelegramTransport(
const nextAttempt = transportAttempts[nextIndex];
if (nextAttempt.logMessage) {
const reasonText = reason ? `, reason=${reason}` : "";
log.warn(`${nextAttempt.logMessage} (codes=${formatErrorCodes(err)}${reasonText})`);
const logLine = `${nextAttempt.logMessage} (codes=${formatErrorCodes(err)}${reasonText})`;
if (nextAttempt.logLevel === "debug") {
log.debug(logLine);
} else {
log.warn(logLine);
}
}
stickyAttemptIndex = nextIndex;
return true;

View File

@@ -202,31 +202,53 @@ describe("resolveTelegramDnsResultOrderDecision", () => {
name: "ignores invalid env and config values before applying Node 22 default",
env: { OPENCLAW_TELEGRAM_DNS_RESULT_ORDER: "bogus" },
network: { dnsResultOrder: "invalid" } as unknown as TelegramNetworkConfig,
defaultResultOrder: "ipv6first",
nodeMajor: 22,
expected: { value: "ipv4first", source: "default-node22" },
},
{
name: "inherits process default when env and config are unset",
defaultResultOrder: "ipv4first",
nodeMajor: 20,
expected: { value: "ipv4first", source: "process-default" },
},
{
name: "prefers config over process default",
network: { dnsResultOrder: "verbatim" },
defaultResultOrder: "ipv4first",
nodeMajor: 20,
expected: { value: "verbatim", source: "config" },
},
] satisfies Array<{
name: string;
env?: NodeJS.ProcessEnv;
network?: TelegramNetworkConfig;
defaultResultOrder?: string | null;
nodeMajor: number;
expected: ReturnType<typeof resolveTelegramDnsResultOrderDecision>;
}>)("$name", ({ env, network, nodeMajor, expected }) => {
}>)("$name", ({ env, network, defaultResultOrder, nodeMajor, expected }) => {
const decision = resolveTelegramDnsResultOrderDecision({
env,
network,
defaultResultOrder,
nodeMajor,
});
expect(decision).toEqual(expected);
});
it("defaults to ipv4first on Node 22", () => {
const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 22 });
const decision = resolveTelegramDnsResultOrderDecision({
defaultResultOrder: null,
nodeMajor: 22,
});
expect(decision).toEqual({ value: "ipv4first", source: "default-node22" });
});
it("returns null when no dns decision applies", () => {
const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 20 });
const decision = resolveTelegramDnsResultOrderDecision({
defaultResultOrder: null,
nodeMajor: 20,
});
expect(decision).toEqual({ value: null });
});
});

View File

@@ -1,3 +1,4 @@
import * as dns from "node:dns";
import process from "node:process";
import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-types";
import { isTruthyEnvValue, isWSL2Sync } from "openclaw/plugin-sdk/runtime-env";
@@ -66,12 +67,14 @@ export function resolveTelegramAutoSelectFamilyDecision(params?: {
* Priority:
* 1. Environment variable OPENCLAW_TELEGRAM_DNS_RESULT_ORDER
* 2. Config: channels.telegram.network.dnsResultOrder
* 3. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues)
* 3. Process default: dns.getDefaultResultOrder()
* 4. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues)
*/
export function resolveTelegramDnsResultOrderDecision(params?: {
network?: TelegramNetworkConfig;
env?: NodeJS.ProcessEnv;
nodeMajor?: number;
defaultResultOrder?: string | null;
}): TelegramDnsResultOrderDecision {
const env = params?.env ?? process.env;
const nodeMajor =
@@ -93,6 +96,15 @@ export function resolveTelegramDnsResultOrderDecision(params?: {
return { value: configValue, source: "config" };
}
const processDefaultValue = normalizeOptionalLowercaseString(
params && "defaultResultOrder" in params
? params.defaultResultOrder
: dns.getDefaultResultOrder?.(),
);
if (processDefaultValue === "ipv4first" || processDefaultValue === "verbatim") {
return { value: processDefaultValue, source: "process-default" };
}
// Default to ipv4first on Node 22+ to avoid IPv6 issues
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) {
return { value: "ipv4first", source: "default-node22" };