{"record":{"id":"423727de65b320e4","repo":"zeroclaw-labs/zeroclaw","slug":"telegram-sendmessage-failed-markdown-plai","errorCode":null,"errorMessage":"Telegram sendMessage failed (markdown {}: {}; plain {}: {})","messagePattern":"Telegram sendMessage failed \\(markdown (.+?): (.+?); plain (.+?): (.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/telegram.rs","lineNumber":2989,"sourceCode":"                \"chat_id\": chat_id,\n                \"text\": text,\n            });\n\n            // Add message_thread_id for forum topic support\n            if let Some(tid) = thread_id {\n                plain_body[\"message_thread_id\"] = serde_json::Value::String(tid.to_string());\n            }\n            let plain_resp = self\n                .http_client()\n                .post(self.api_url(\"sendMessage\"))\n                .json(&plain_body)\n                .send()\n                .await?;\n\n            if !plain_resp.status().is_success() {\n                let plain_status = plain_resp.status();\n                let plain_err = plain_resp.text().await.unwrap_or_default();\n                anyhow::bail!(\n                    \"Telegram sendMessage failed (markdown {}: {}; plain {}: {})\",\n                    markdown_status,\n                    markdown_err,\n                    plain_status,\n                    plain_err\n                );\n            }\n\n            if index < chunks.len() - 1 {\n                tokio::time::sleep(Duration::from_millis(100)).await;\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn send_media_by_url(\n        &self,","sourceCodeStart":2971,"sourceCodeEnd":3007,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/telegram.rs#L2971-L3007","documentation":"send_text_chunks first sends each chunk as Telegram HTML (markdown_to_telegram_html with parse_mode=HTML) and, if that attempt fails, retries the identical chunk as plain text; this error fires only when BOTH attempts fail, embedding both statuses and bodies. The plain attempt's body is authoritative — it names why the chat itself is undeliverable.","triggerScenarios":"403 'bot was blocked by the user' or 'bot was kicked from the group chat' (both modes fail identically); 429 flood control while chunking long replies; 400 'message is too long' when a single chunk still exceeds 4096 characters; chat_id for a chat that never existed.","commonSituations":"User blocked the bot then triggered a background/cron reply; bot removed from a group but still in the paired chat list; burst replies hitting per-chat limits; upstream chunker producing oversized chunks.","solutions":["Read the plain attempt's `description` in the message — it identifies the terminal cause.","403 blocked/kicked: stop sending to that chat; the user must re-open the chat (find the chat via /start) before delivery can succeed.","429: honor retry_after, resend, and keep the existing 100ms inter-chunk spacing or increase it.","400 too long: split text into chunks of at most 4096 characters before calling send_text_chunks."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const TG_LIMIT: usize = 4096;\nanyhow::ensure!(chunk.chars().count() <= TG_LIMIT, \"chunk exceeds 4096 chars\");","typeGuard":"fn is_chat_undeliverable(err: &anyhow::Error) -> bool {\n    let s = err.to_string();\n    s.contains(\"bot was blocked\") || s.contains(\"chat not found\") || s.contains(\"kicked\")\n}","tryCatchPattern":"if let Err(e) = channel.send_text_chunks(text, chat, thread).await {\n    if is_chat_undeliverable(&e) {\n        mark_chat_disabled(chat).await; // stop future sends\n        return Ok(());\n    }\n    if e.to_string().contains(\"429\") {\n        tokio::time::sleep(Duration::from_secs(2)).await;\n        return channel.send_text_chunks(text, chat, thread).await;\n    }\n    return Err(e);\n}","preventionTips":["Split outgoing text into <=4096-char chunks yourself; do not rely on the API to accept oversized text.","Track blocked/kicked chats from 403 responses and prune them from the paired list.","Pace chunked replies to stay under flood-control limits."],"tags":["telegram","sendmessage","html-parse-mode","rate-limit","blocked"],"backgroundTag":"telegram-sendmessage-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}