zeroclaw-labs/zeroclaw · error

Telegram sendMessage (approval) failed ({status}): {err}

Error message

Telegram sendMessage (approval) failed ({status}): {err}

What it means

Raised by the Telegram channel's tool-approval flow when the fallback plain-text sendMessage to the Telegram Bot API returns a non-2xx status. The first send attempt (HTML parse_mode) already failed, the code retried without parse_mode keeping the inline approve/deny buttons, and that retry also failed. The pending approval entry is removed from the map before bailing, so the tool call that requested approval fails without ever getting an operator verdict.

Source

Thrown at crates/zeroclaw-channels/src/telegram.rs:4683

                });
                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;

                match plain_resp {
                    Ok(r) if r.status().is_success() => true,
                    Ok(r) => {
                        let status = r.status();
                        let err = r.text().await.unwrap_or_default();
                        self.pending_approvals.lock().await.remove(&approval_id);
                        anyhow::bail!("Telegram sendMessage (approval) failed ({status}): {err}");
                    }
                    Err(e) => {
                        self.pending_approvals.lock().await.remove(&approval_id);
                        return Err(e.into());
                    }
                }
            }
            Err(e) => {
                self.pending_approvals.lock().await.remove(&approval_id);
                return Err(e.into());
            }
        };

        if !send_ok {
            self.pending_approvals.lock().await.remove(&approval_id);
            anyhow::bail!("Telegram sendMessage (approval) failed after fallback");
        }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the {status} in the message: 401/404 means the bot token is wrong or revoked — update channels.telegram bot token and verify with a getMe call
  2. 400 means Telegram rejects the request — verify chat_id is correct, the user has pressed Start on the bot, and message_thread_id (if set) refers to an existing topic
  3. 429 means rate limited — reduce approval volume and honor the retry_after value Telegram returns in {err}
  4. Check the preceding WARN log 'Telegram sendMessage (approval) with HTML failed; retrying without parse_mode' — its status/err attrs show the first failure and tell you whether HTML markup or delivery itself was the problem
  5. If a custom API base URL or proxy is configured for Telegram, verify it reaches the real Bot API

Example fix

# before: token revoked by regenerating in BotFather
# zeroclaw.toml: channels.telegram.bot_token = "123456:AA-old-revoked-token"

curl -s "https://api.telegram.org/bot<NEW_TOKEN>/getMe"
# {"ok":true,"result":{"is_bot":true,"first_name":"..."}}

# after
# zeroclaw.toml: channels.telegram.bot_token = "123456:AA-new-valid-token"
Defensive patterns

Strategy: try-catch

Validate before calling

// Startup health check: verify the bot token can talk to Telegram
async fn telegram_token_ok(http: &reqwest::Client, token: &str) -> bool {
    let url = format!("https://api.telegram.org/bot{token}/getMe");
    http.post(&url).send().await
        .map(|r| r.status().is_success())
        .unwrap_or(false)
}

Try / catch

match approval_request.await {
    Ok(resp) => { /* proceed */ }
    Err(e) => {
        let msg = e.to_string();
        if msg.contains("401") || msg.contains("404") {
            // fatal config problem: alert, do not retry
            tracing::error!("telegram bot token invalid: {msg}");
        } else if msg.contains("429") {
            // rate limited: back off and re-request approval later
            tokio::time::sleep(retry_after).await;
        } else {
            // treat as delivery failure: fall back to deny or another channel
        }
    }
}

Prevention

When it happens

Trigger: POST to api.telegram.org/bot<token>/sendMessage for an approval prompt returns non-success on both the HTML attempt and the plain-text fallback: 401 Unauthorized (bad or revoked bot token), 400 Bad Request (unknown chat_id, invalid message_thread_id, or user never pressed Start / blocked the bot), 404 (malformed token shape), 429 (rate limit hit).

Common situations: Bot token regenerated via BotFather so the configured channels.telegram token is now revoked; wrong chat_id or approval chat config; user blocked the bot or never initiated the chat; message_thread_id pointing at a deleted forum topic; bursts of simultaneous approval prompts hitting Telegram rate limits.

Related errors


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