From ce15f348bbc419e91c8ab0034cf2a06382f5e912 Mon Sep 17 00:00:00 2001
From: miorbnli
Date: Sun, 28 Jun 2026 00:31:52 +0800
Subject: [PATCH] fix(telegram): use idempotent retry context for
delete/reaction (#96612)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
reactMessageTelegram and deleteMessageTelegram passed context: "send" to
isRecoverableTelegramNetworkError, which disables message-snippet matching
(allowMessageMatch defaults to false only for "send"). Both operations are
idempotent (setMessageReaction / deleteMessage are safe to repeat), yet a
transient snippet-only network error (e.g. "socket hang up", "undici network
error" with no error code) was not retried — stricter than polling/webhook/
unknown, which all default allowMessageMatch to true. Users saw spurious
reaction/delete failures on transient network errors.
Add delete | react to TelegramNetworkErrorContext (additive) and use them at
the two callers. The helper default (context !== "send") is unchanged, so
delete/react now match polling/webhook/unknown. sendMessage keeps "send".
Co-authored-by: Claude
---
extensions/telegram/src/network-errors.test.ts | 10 ++++++++++
extensions/telegram/src/network-errors.ts | 8 +++++++-
extensions/telegram/src/send.ts | 4 ++--
3 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/extensions/telegram/src/network-errors.test.ts b/extensions/telegram/src/network-errors.test.ts
index d57380524da..498fd5418a1 100644
--- a/extensions/telegram/src/network-errors.test.ts
+++ b/extensions/telegram/src/network-errors.test.ts
@@ -139,6 +139,16 @@ describe("isRecoverableTelegramNetworkError", () => {
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true);
});
+ it("treats delete/react (idempotent) contexts like polling, not send", () => {
+ const undiciSnippetErr = new Error("Undici: socket failure");
+ // delete and react are idempotent Telegram operations; a transient
+ // snippet-only error must be retried (allowMessageMatch defaults true),
+ // matching polling/webhook. send stays strict as the regression guard.
+ expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "delete" })).toBe(true);
+ expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "react" })).toBe(true);
+ expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false);
+ });
+
it("treats grammY failed-after envelope errors as recoverable in send context", () => {
expect(
isRecoverableTelegramNetworkError(
diff --git a/extensions/telegram/src/network-errors.ts b/extensions/telegram/src/network-errors.ts
index d8ade9fdffa..9fd24633f9d 100644
--- a/extensions/telegram/src/network-errors.ts
+++ b/extensions/telegram/src/network-errors.ts
@@ -141,7 +141,13 @@ export function isTelegramMisdirectedRequestError(err: unknown): boolean {
return false;
}
-export type TelegramNetworkErrorContext = "polling" | "send" | "webhook" | "unknown";
+export type TelegramNetworkErrorContext =
+ | "polling"
+ | "send"
+ | "webhook"
+ | "delete"
+ | "react"
+ | "unknown";
export type TelegramNetworkErrorOrigin = {
method?: string | null;
url?: string | null;
diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts
index 33adeeffdb8..6ae8891fc86 100644
--- a/extensions/telegram/src/send.ts
+++ b/extensions/telegram/src/send.ts
@@ -1219,7 +1219,7 @@ export async function reactMessageTelegram(
account,
retry: opts.retry,
verbose: opts.verbose,
- shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "react" }),
});
const remove = opts.remove === true;
const trimmedEmoji = emoji.trim();
@@ -1276,7 +1276,7 @@ export async function deleteMessageTelegram(
account,
retry: opts.retry,
verbose: opts.verbose,
- shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "delete" }),
});
try {
await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage", {