From 67b5bce44f7014c8cbefc00eed0731e61d6300b9 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 22 Feb 2026 16:09:59 +0530 Subject: [PATCH] fix: distill telegram fetch-failed retry rules --- CHANGELOG.md | 1 + src/telegram/network-errors.test.ts | 10 +++++++--- src/telegram/network-errors.ts | 24 ++++++++---------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ad0412d9d..ea5223314f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai - Plugins/CLI: make `openclaw plugins enable` and plugin install/link flows update allowlists via shared plugin-enable policy so enabled plugins are not left disabled by allowlist mismatch. (#23190) Thanks @downwind7clawd-ctrl. - Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07. - Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces. +- Telegram/Retry: classify undici `TypeError: fetch failed` as recoverable in both polling and send retry paths so transient fetch failures no longer fail fast. (#16699) thanks @Glucksberg. - BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines. - BlueBubbles/Webhooks: accept inbound/reaction webhook payloads when BlueBubbles omits `handle` but provides DM `chatGuid`, and harden payload extraction for array/string-wrapped message bodies so valid webhook events no longer get rejected as unparseable. (#23275) Thanks @toph31. - Security/Audit: add `openclaw security audit` finding `gateway.nodes.allow_commands_dangerous` for risky `gateway.nodes.allowCommands` overrides, with severity upgraded to critical on remote gateway exposure. diff --git a/src/telegram/network-errors.test.ts b/src/telegram/network-errors.test.ts index 358899a80a6..b92081a8284 100644 --- a/src/telegram/network-errors.test.ts +++ b/src/telegram/network-errors.test.ts @@ -40,9 +40,13 @@ describe("isRecoverableTelegramNetworkError", () => { }); it("skips broad message matches for send context", () => { - const err = new Error("Network request for 'sendMessage' failed!"); - expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false); - expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true); + const networkRequestErr = new Error("Network request for 'sendMessage' failed!"); + expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "send" })).toBe(false); + expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "polling" })).toBe(true); + + const undiciSnippetErr = new Error("Undici: socket failure"); + expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false); + expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true); }); it("returns false for unrelated errors", () => { diff --git a/src/telegram/network-errors.ts b/src/telegram/network-errors.ts index 1532d6a42ea..177ef00d646 100644 --- a/src/telegram/network-errors.ts +++ b/src/telegram/network-errors.ts @@ -27,9 +27,9 @@ const RECOVERABLE_ERROR_NAMES = new Set([ "BodyTimeoutError", ]); +const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]); + const RECOVERABLE_MESSAGE_SNIPPETS = [ - "fetch failed", - "typeerror: fetch failed", "undici", "network error", "network request", @@ -40,13 +40,6 @@ const RECOVERABLE_MESSAGE_SNIPPETS = [ "timed out", // grammY getUpdates returns "timed out after X seconds" (not matched by "timeout") ]; -// Undici surface errors as TypeError("fetch failed") with optional nested causes. -// Treat this exact shape as recoverable even when broad message matching is disabled. -function isUndiciFetchFailedError(err: unknown): boolean { - const message = formatErrorMessage(err).trim().toLowerCase(); - return message === "fetch failed" || message === "typeerror: fetch failed"; -} - function normalizeCode(code?: string): string { return code?.trim().toUpperCase() ?? ""; } @@ -135,10 +128,6 @@ export function isRecoverableTelegramNetworkError( : options.context !== "send"; for (const candidate of collectErrorCandidates(err)) { - if (isUndiciFetchFailedError(candidate)) { - return true; - } - const code = normalizeCode(getErrorCode(candidate)); if (code && RECOVERABLE_ERROR_CODES.has(code)) { return true; @@ -149,9 +138,12 @@ export function isRecoverableTelegramNetworkError( return true; } - if (allowMessageMatch) { - const message = formatErrorMessage(candidate).toLowerCase(); - if (message && RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) { + const message = formatErrorMessage(candidate).trim().toLowerCase(); + if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) { + return true; + } + if (allowMessageMatch && message) { + if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) { return true; } }