Skip to content

Commit 67b5bce

Browse files
committed
fix: distill telegram fetch-failed retry rules
1 parent 0452791 commit 67b5bce

File tree

3 files changed

+16
-19
lines changed

3 files changed

+16
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
6262
- 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.
6363
- 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.
6464
- Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`, `[[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.
65+
- 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.
6566
- BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines.
6667
- 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.
6768
- 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.

src/telegram/network-errors.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ describe("isRecoverableTelegramNetworkError", () => {
4040
});
4141

4242
it("skips broad message matches for send context", () => {
43-
const err = new Error("Network request for 'sendMessage' failed!");
44-
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);
45-
expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true);
43+
const networkRequestErr = new Error("Network request for 'sendMessage' failed!");
44+
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "send" })).toBe(false);
45+
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "polling" })).toBe(true);
46+
47+
const undiciSnippetErr = new Error("Undici: socket failure");
48+
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false);
49+
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true);
4650
});
4751

4852
it("returns false for unrelated errors", () => {

src/telegram/network-errors.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ const RECOVERABLE_ERROR_NAMES = new Set([
2727
"BodyTimeoutError",
2828
]);
2929

30+
const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
31+
3032
const RECOVERABLE_MESSAGE_SNIPPETS = [
31-
"fetch failed",
32-
"typeerror: fetch failed",
3333
"undici",
3434
"network error",
3535
"network request",
@@ -40,13 +40,6 @@ const RECOVERABLE_MESSAGE_SNIPPETS = [
4040
"timed out", // grammY getUpdates returns "timed out after X seconds" (not matched by "timeout")
4141
];
4242

43-
// Undici surface errors as TypeError("fetch failed") with optional nested causes.
44-
// Treat this exact shape as recoverable even when broad message matching is disabled.
45-
function isUndiciFetchFailedError(err: unknown): boolean {
46-
const message = formatErrorMessage(err).trim().toLowerCase();
47-
return message === "fetch failed" || message === "typeerror: fetch failed";
48-
}
49-
5043
function normalizeCode(code?: string): string {
5144
return code?.trim().toUpperCase() ?? "";
5245
}
@@ -135,10 +128,6 @@ export function isRecoverableTelegramNetworkError(
135128
: options.context !== "send";
136129

137130
for (const candidate of collectErrorCandidates(err)) {
138-
if (isUndiciFetchFailedError(candidate)) {
139-
return true;
140-
}
141-
142131
const code = normalizeCode(getErrorCode(candidate));
143132
if (code && RECOVERABLE_ERROR_CODES.has(code)) {
144133
return true;
@@ -149,9 +138,12 @@ export function isRecoverableTelegramNetworkError(
149138
return true;
150139
}
151140

152-
if (allowMessageMatch) {
153-
const message = formatErrorMessage(candidate).toLowerCase();
154-
if (message && RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) {
141+
const message = formatErrorMessage(candidate).trim().toLowerCase();
142+
if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) {
143+
return true;
144+
}
145+
if (allowMessageMatch && message) {
146+
if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) {
155147
return true;
156148
}
157149
}

0 commit comments

Comments
 (0)