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

  1. Read the embedded body — for streaming replies this is the first signal that the chat is undeliverable.
  2. 403 blocked/kicked: disable or drop that chat; delivery cannot resume until the user re-initiates.
  3. 429: honor retry_after, resend the draft, and pace subsequent edits.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/9ee26f3be4f96028. Report an issue: GitHub.