From e5af5d7c39814baba712b184a9d188bd8e72797d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sun, 31 May 2026 14:28:08 +0000 Subject: [PATCH 1/3] fix(telegram): retry sendMessage as plain text on Markdown parse failure Fixes #871 The Telegram adapter silently dropped messages when Telegram rejected Markdown formatting (unescaped _, *, [, ] etc). Now checks the API response body and retries without parse_mode on failure. --- gateway/src/adapters/telegram.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/gateway/src/adapters/telegram.rs b/gateway/src/adapters/telegram.rs index ee5d08dd1..04c9fb9e1 100644 --- a/gateway/src/adapters/telegram.rs +++ b/gateway/src/adapters/telegram.rs @@ -332,17 +332,39 @@ pub async fn handle_reply( "gateway → telegram" ); let url = format!("{TELEGRAM_API_BASE}/bot{bot_token}/sendMessage"); - let _ = client + let resp = client .post(&url) .json(&serde_json::json!({ "chat_id": reply.channel.id, - "text": reply.content.text, + "text": &reply.content.text, "message_thread_id": reply.channel.thread_id, "parse_mode": "Markdown", })) .send() - .await - .map_err(|e| error!("telegram send error: {e}")); + .await; + + match resp { + Ok(r) => { + let body: serde_json::Value = r.json().await.unwrap_or_default(); + if body["ok"].as_bool() != Some(true) { + warn!( + "Markdown send failed: {}, retrying as plain text", + body["description"] + ); + let _ = client + .post(&url) + .json(&serde_json::json!({ + "chat_id": reply.channel.id, + "text": &reply.content.text, + "message_thread_id": reply.channel.thread_id, + })) + .send() + .await + .map_err(|e| error!("telegram plain-text send error: {e}")); + } + } + Err(e) => error!("telegram send error: {e}"), + } } /// Download media from Telegram via getFile → store to filesystem (colocate mode). From 4e8ec3934e46b6208fd1d5f6abbabbd773f2d530 Mon Sep 17 00:00:00 2001 From: shaun-agent Date: Mon, 1 Jun 2026 08:14:48 +0000 Subject: [PATCH 2/3] fix(telegram): scope retry to parse errors, check retry response, fix null desc Addresses review findings from chaodu-agent: - F1: Only retry as plain text when description contains 'parse' or 'entities' (Markdown parse failure). Other errors (429, 401, 403) are logged without retry to avoid worsening rate limits. - F2: Check the retry response body; log error if retry also fails. - F3: Use .as_str().unwrap_or() instead of raw indexing to avoid printing 'Null' in logs. --- gateway/src/adapters/telegram.rs | 44 ++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/gateway/src/adapters/telegram.rs b/gateway/src/adapters/telegram.rs index 04c9fb9e1..10f7cdc67 100644 --- a/gateway/src/adapters/telegram.rs +++ b/gateway/src/adapters/telegram.rs @@ -347,20 +347,36 @@ pub async fn handle_reply( Ok(r) => { let body: serde_json::Value = r.json().await.unwrap_or_default(); if body["ok"].as_bool() != Some(true) { - warn!( - "Markdown send failed: {}, retrying as plain text", - body["description"] - ); - let _ = client - .post(&url) - .json(&serde_json::json!({ - "chat_id": reply.channel.id, - "text": &reply.content.text, - "message_thread_id": reply.channel.thread_id, - })) - .send() - .await - .map_err(|e| error!("telegram plain-text send error: {e}")); + let desc = body["description"].as_str().unwrap_or("unknown error"); + if desc.contains("parse") || desc.contains("entities") { + warn!("Markdown send failed: {desc}, retrying as plain text"); + match client + .post(&url) + .json(&serde_json::json!({ + "chat_id": reply.channel.id, + "text": &reply.content.text, + "message_thread_id": reply.channel.thread_id, + })) + .send() + .await + { + Ok(retry_r) => { + let retry_body: serde_json::Value = + retry_r.json().await.unwrap_or_default(); + if retry_body["ok"].as_bool() != Some(true) { + error!( + "telegram plain-text retry failed: {}", + retry_body["description"] + .as_str() + .unwrap_or("unknown error") + ); + } + } + Err(e) => error!("telegram plain-text send error: {e}"), + } + } else { + error!("telegram send failed: {desc}"); + } } } Err(e) => error!("telegram send error: {e}"), From 19ebe0ef01e253224c149bd12c1d1440921d5a03 Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 1 Jun 2026 08:15:39 +0000 Subject: [PATCH 3/3] test(telegram): extract markdown error check and add tests --- gateway/src/adapters/telegram.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/gateway/src/adapters/telegram.rs b/gateway/src/adapters/telegram.rs index 10f7cdc67..af43735fd 100644 --- a/gateway/src/adapters/telegram.rs +++ b/gateway/src/adapters/telegram.rs @@ -213,6 +213,13 @@ pub async fn webhook( axum::http::StatusCode::OK } +fn is_markdown_parse_error(description: &str) -> bool { + let desc_lower = description.to_lowercase(); + desc_lower.contains("can't find end") + || desc_lower.contains("can't parse") + || desc_lower.contains("parse entities") +} + // --- Reply handler --- pub async fn handle_reply( @@ -348,7 +355,7 @@ pub async fn handle_reply( let body: serde_json::Value = r.json().await.unwrap_or_default(); if body["ok"].as_bool() != Some(true) { let desc = body["description"].as_str().unwrap_or("unknown error"); - if desc.contains("parse") || desc.contains("entities") { + if is_markdown_parse_error(desc) { warn!("Markdown send failed: {desc}, retrying as plain text"); match client .post(&url) @@ -521,3 +528,17 @@ async fn download_telegram_document( path: Some(path), }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_markdown_parse_error() { + assert!(is_markdown_parse_error("Bad Request: can't find end of italic entity at byte offset 37")); + assert!(is_markdown_parse_error("Bad Request: can't parse entities: Can't find end of bold entity")); + assert!(is_markdown_parse_error("can't parse entities in message text")); + assert!(!is_markdown_parse_error("Unauthorized")); + assert!(!is_markdown_parse_error("Bad Request: chat not found")); + } +}