zeroclaw-labs/zeroclaw · error
Telegram sendMessage failed (markdown {}: {}; plain {}: {})
Error message
Telegram sendMessage failed (markdown {}: {}; plain {}: {}) What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/telegram.rs:2989
"chat_id": chat_id,
"text": text,
});
// Add message_thread_id for forum topic support
if let Some(tid) = thread_id {
plain_body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
let plain_resp = self
.http_client()
.post(self.api_url("sendMessage"))
.json(&plain_body)
.send()
.await?;
if !plain_resp.status().is_success() {
let plain_status = plain_resp.status();
let plain_err = plain_resp.text().await.unwrap_or_default();
anyhow::bail!(
"Telegram sendMessage failed (markdown {}: {}; plain {}: {})",
markdown_status,
markdown_err,
plain_status,
plain_err
);
}
if index < chunks.len() - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
Ok(())
}
async fn send_media_by_url(
&self,View on GitHub (pinned to 88bb9c8533)
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.
Defensive patterns
Strategy: retry
Validate before calling
const TG_LIMIT: usize = 4096; anyhow::ensure!(chunk.chars().count() <= TG_LIMIT, "chunk exceeds 4096 chars");
Type guard
fn is_chat_undeliverable(err: &anyhow::Error) -> bool {
let s = err.to_string();
s.contains("bot was blocked") || s.contains("chat not found") || s.contains("kicked")
} Try / catch
if let Err(e) = channel.send_text_chunks(text, chat, thread).await {
if is_chat_undeliverable(&e) {
mark_chat_disabled(chat).await; // stop future sends
return Ok(());
}
if e.to_string().contains("429") {
tokio::time::sleep(Duration::from_secs(2)).await;
return channel.send_text_chunks(text, chat, thread).await;
}
return Err(e);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Telegram sendMessage (draft) failed: {err}
- {method} failed: status={status}, body={body}
- channel does not support room creation
- post failed ({status}): {body}
- interaction followup post failed ({status}): {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/423727de65b320e4.
Report an issue: GitHub.