zeroclaw-labs/zeroclaw · error
Telegram sendMessage (draft) failed: {err}
Error message
Telegram sendMessage (draft) failed: {err} What it means
send_draft() is the streaming-mode path: before streaming edits begin it posts the initial placeholder message ('...' when content is empty) as a plain JSON sendMessage and bails with Telegram's body (empty string if the body stream is unreadable) on non-2xx. The returned message_id is what later streaming edits target, so failure here aborts the whole streamed reply.
Source
Thrown at crates/zeroclaw-channels/src/telegram.rs:3863
let mut body = serde_json::json!({
"chat_id": chat_id,
"text": initial_text,
});
if let Some(tid) = thread_id {
body["message_thread_id"] = serde_json::Value::String(tid.to_string());
}
let resp = self
.client
.post(self.api_url("sendMessage"))
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let err = resp.text().await.unwrap_or_default();
anyhow::bail!("Telegram sendMessage (draft) failed: {err}");
}
let resp_json: serde_json::Value = resp.json().await?;
let message_id = resp_json
.get("result")
.and_then(|r| r.get("message_id"))
.and_then(|id| id.as_i64())
.map(|id| id.to_string());
self.last_draft_edit
.lock()
.insert(chat_id.to_string(), std::time::Instant::now());
Ok(message_id)
}
async fn update_draft(
&self,View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the embedded body — for streaming replies this is the first signal that the chat is undeliverable.
- 403 blocked/kicked: disable or drop that chat; delivery cannot resume until the user re-initiates.
- 429: honor retry_after, resend the draft, and pace subsequent edits.
- Verify bot_token with getMe when drafts fail across all chats.
Defensive patterns
Strategy: try-catch
Validate before calling
let (chat_id, thread_id) = TelegramChannel::parse_reply_target(&message.recipient); anyhow::ensure!(!chat_id.is_empty(), "recipient produced empty chat_id");
Type guard
fn is_draft_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) = telegram.send(message).await {
if e.to_string().contains("(draft) failed") && is_draft_chat_undeliverable(&e) {
disable_chat(&message.recipient).await; // stop retrying dead chats
return Ok(());
}
return Err(e);
} Prevention
- Treat draft-send 403s as a signal to prune the paired chat, since every streamed reply starts with this call.
- Pace streamed replies to avoid 429 on the initial draft sendMessage.
- Validate the parsed chat_id is non-empty before posting.
When it happens
Trigger: 403 'bot was blocked by the user' or chat not found — the draft is the first send of a reply, so chat-level problems surface here first; 429 flood control at reply start; 400 when recipient parsing produced a malformed chat_id; api_base/token problems.
Common situations: User blocked the bot right before a (streamed) reply fired; per-chat rate limit hit by back-to-back streamed replies; recipient string malformed after pairing data changes; wrong token after rotation.
Related errors
- Telegram sendMessage failed (markdown {}: {}; plain {}: {})
- chat.postMessage (lazy draft) failed: {err}
- {method} failed: status={status}, body={body}
- channel does not support room creation
- post failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9ee26f3be4f96028.
Report an issue: GitHub.