From 51b3e23680da96b6243a610c66a4cb5850ce7c72 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Sun, 15 Feb 2026 00:45:49 +0000 Subject: [PATCH] fix(telegram): fallback to plain text when threaded markdown renders empty Minimal fix path for Telegram empty-text failures in threaded replies. - fallback to plain text when formatted htmlText is empty - retry plain text on parse/empty-text API errors - add focused regression test for threaded mode case Related: #25091 Supersedes alternative fix path in #17629 if maintainers prefer minimal scope. --- src/telegram/bot/delivery.test.ts | 34 ++++++++++++++++++++++++ src/telegram/bot/delivery.ts | 43 ++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/telegram/bot/delivery.test.ts b/src/telegram/bot/delivery.test.ts index d4606ac1414..27b365b3b1a 100644 --- a/src/telegram/bot/delivery.test.ts +++ b/src/telegram/bot/delivery.test.ts @@ -244,6 +244,40 @@ describe("deliverReplies", () => { ); }); + it("falls back to plain text when markdown renders to empty HTML in threaded mode", async () => { + const runtime = { error: vi.fn(), log: vi.fn() }; + const sendMessage = vi.fn(async (_chatId: string, text: string) => { + if (text === "") { + throw new Error("400: Bad Request: message text is empty"); + } + return { + message_id: 6, + chat: { id: "123" }, + }; + }); + const bot = { api: { sendMessage } } as unknown as Bot; + + await deliverReplies({ + replies: [{ text: ">" }], + chatId: "123", + token: "tok", + runtime, + bot, + replyToMode: "off", + textLimit: 4000, + thread: { id: 42, scope: "forum" }, + }); + + expect(sendMessage).toHaveBeenCalledTimes(1); + expect(sendMessage).toHaveBeenCalledWith( + "123", + ">", + expect.objectContaining({ + message_thread_id: 42, + }), + ); + }); + 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 5e0cfb2ea1f..e515c686d88 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -41,6 +41,7 @@ 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[]; @@ -553,6 +554,30 @@ async function sendTelegramText( const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true }; const textMode = opts?.textMode ?? "markdown"; const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text); + 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, + fn: () => + bot.api.sendMessage(chatId, fallbackText, { + ...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}), + ...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}), + ...baseParams, + }), + }); + 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. + if (!htmlText.trim()) { + return await sendPlainFallback(); + } try { const res = await withTelegramApiErrorLogging({ operation: "sendMessage", @@ -570,21 +595,9 @@ async function sendTelegramText( return res.message_id; } catch (err) { const errText = formatErrorMessage(err); - if (PARSE_ERR_RE.test(errText)) { - runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`); - const fallbackText = opts?.plainText ?? text; - const res = await withTelegramApiErrorLogging({ - operation: "sendMessage", - runtime, - fn: () => - bot.api.sendMessage(chatId, fallbackText, { - ...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}), - ...(opts?.replyMarkup ? { reply_markup: opts.replyMarkup } : {}), - ...baseParams, - }), - }); - runtime.log?.(`telegram sendMessage ok chat=${chatId} message=${res.message_id} (plain)`); - return res.message_id; + if (PARSE_ERR_RE.test(errText) || EMPTY_TEXT_ERR_RE.test(errText)) { + runtime.log?.(`telegram formatted send failed; retrying without formatting: ${errText}`); + return await sendPlainFallback(); } throw err; }