From 6e31bca1987c47f8d8073943c3173884006a2728 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 24 Feb 2026 22:23:17 +0530 Subject: [PATCH] fix(telegram): fail loud on empty text fallback --- src/telegram/bot/delivery.test.ts | 19 +++++++++++++++++++ src/telegram/bot/delivery.ts | 17 ++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/telegram/bot/delivery.test.ts b/src/telegram/bot/delivery.test.ts index 27b365b3b1a..846f5b409db 100644 --- a/src/telegram/bot/delivery.test.ts +++ b/src/telegram/bot/delivery.test.ts @@ -278,6 +278,25 @@ describe("deliverReplies", () => { ); }); + it("throws when formatted and plain fallback text are both empty", async () => { + const runtime = { error: vi.fn(), log: vi.fn() }; + const sendMessage = vi.fn(); + const bot = { api: { sendMessage } } as unknown as Bot; + + await expect( + deliverReplies({ + replies: [{ text: " " }], + chatId: "123", + token: "tok", + runtime, + bot, + replyToMode: "off", + textLimit: 4000, + }), + ).rejects.toThrow("empty formatted text and empty plain fallback"); + expect(sendMessage).not.toHaveBeenCalled(); + }); + it("uses reply_to_message_id when quote text is provided", async () => { const runtime = createRuntime(); const sendMessage = vi.fn().mockResolvedValue({ diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index 937c8483ec1..748fca00a4d 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -33,6 +33,7 @@ import { import type { StickerMetadata, TelegramContext } from "./types.js"; const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; +const EMPTY_TEXT_ERR_RE = /message text is empty/i; const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/; const FILE_TOO_BIG_RE = /file is too big/i; const TELEGRAM_MEDIA_SSRF_POLICY = { @@ -41,7 +42,6 @@ const TELEGRAM_MEDIA_SSRF_POLICY = { allowedHostnames: ["api.telegram.org"], allowRfc2544BenchmarkRange: true, }; -const EMPTY_TEXT_ERR_RE = /message text is empty/i; export async function deliverReplies(params: { replies: ReplyPayload[]; @@ -544,7 +544,7 @@ async function sendTelegramText( linkPreview?: boolean; replyMarkup?: ReturnType; }, -): Promise { +): Promise { const baseParams = buildTelegramSendParams({ replyToMessageId: opts?.replyToMessageId, thread: opts?.thread, @@ -557,9 +557,6 @@ async function sendTelegramText( const fallbackText = opts?.plainText ?? text; const hasFallbackText = fallbackText.trim().length > 0; const sendPlainFallback = async () => { - if (!hasFallbackText) { - return undefined; - } const res = await withTelegramApiErrorLogging({ operation: "sendMessage", runtime, @@ -570,12 +567,15 @@ async function sendTelegramText( ...baseParams, }), }); + runtime.log?.(`telegram sendMessage ok chat=${chatId} message=${res.message_id} (plain)`); return res.message_id; }; - // Markdown can occasionally render to empty HTML (for example syntax-only chunks). - // Telegram rejects those sends, so fall back to plain text early. + // Markdown can render to empty HTML for syntax-only chunks; recover with plain text. if (!htmlText.trim()) { + if (!hasFallbackText) { + throw new Error("telegram sendMessage failed: empty formatted text and empty plain fallback"); + } return await sendPlainFallback(); } try { @@ -599,6 +599,9 @@ async function sendTelegramText( } catch (err) { const errText = formatErrorMessage(err); if (PARSE_ERR_RE.test(errText) || EMPTY_TEXT_ERR_RE.test(errText)) { + if (!hasFallbackText) { + throw err; + } runtime.log?.(`telegram formatted send failed; retrying without formatting: ${errText}`); return await sendPlainFallback(); }