diff --git a/gateway/src/adapters/telegram.rs b/gateway/src/adapters/telegram.rs index ee5d08dd1..5ce433007 100644 --- a/gateway/src/adapters/telegram.rs +++ b/gateway/src/adapters/telegram.rs @@ -332,7 +332,7 @@ pub async fn handle_reply( "gateway → telegram" ); let url = format!("{TELEGRAM_API_BASE}/bot{bot_token}/sendMessage"); - let _ = client + let retry_plain = match client .post(&url) .json(&serde_json::json!({ "chat_id": reply.channel.id, @@ -342,7 +342,68 @@ pub async fn handle_reply( })) .send() .await - .map_err(|e| error!("telegram send error: {e}")); + { + Ok(resp) => { + match resp.json::().await { + Ok(body) if body["ok"].as_bool() != Some(true) => { + let desc = body["description"].as_str().unwrap_or("unknown"); + let code = body["error_code"].as_u64().unwrap_or(0); + if should_retry_plain_text_fallback(code, desc) { + warn!(code, desc = %desc, "telegram Markdown parse failed, retrying as plain text"); + true + } else { + error!(code, desc = %desc, "telegram send failed"); + false + } + } + Ok(_) => false, + Err(e) => { + warn!("telegram response not valid JSON: {e}"); + false + } + } + } + Err(e) => { + error!("telegram send error: {e}"); + false + } + }; + + if retry_plain { + 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(resp) => { + match resp.json::().await { + Ok(body) if body["ok"].as_bool() != Some(true) => { + error!( + desc = %body["description"].as_str().unwrap_or("unknown"), + "telegram plain text fallback also failed" + ); + } + Err(e) => warn!("telegram plain text fallback response not valid JSON: {e}"), + _ => {} + } + } + Err(e) => error!("telegram plain text send error: {e}"), + } + } +} + +fn should_retry_plain_text_fallback(code: u64, description: &str) -> bool { + if code != 400 { + return false; + } + + let description = description.to_ascii_lowercase(); + description.contains("parse") || description.contains("entities") } /// Download media from Telegram via getFile → store to filesystem (colocate mode). @@ -483,3 +544,40 @@ async fn download_telegram_document( path: Some(path), }) } + +#[cfg(test)] +mod tests { + use super::should_retry_plain_text_fallback; + + #[test] + fn retries_markdown_parse_errors() { + assert!(should_retry_plain_text_fallback( + 400, + "Bad Request: can't parse entities: Can't find end of the entity" + )); + } + + #[test] + fn retries_entity_errors() { + assert!(should_retry_plain_text_fallback( + 400, + "Bad Request: unsupported start tag in entities" + )); + } + + #[test] + fn does_not_retry_unrelated_bad_requests() { + assert!(!should_retry_plain_text_fallback( + 400, + "Bad Request: message text is empty" + )); + } + + #[test] + fn does_not_retry_non_bad_request_errors() { + assert!(!should_retry_plain_text_fallback( + 429, + "Too Many Requests: retry after 10" + )); + } +}