zeroclaw-labs/zeroclaw · error · anyhow::Error
Discord send message failed ({status}): {err}
Error message
Discord send message failed ({status}): {err} What it means
The core Discord message-send wrapper bails whenever POST /channels/{id}/messages answers non-2xx, embedding both the HTTP status and the response body (or a placeholder when the body itself cannot be read). It backs send, send_discord_message_json, and draft finalize, so nearly every outbound message funnels through it.
Source
Thrown at crates/zeroclaw-channels/src/discord/rest.rs:55
payload: &DiscordOutgoing,
) -> anyhow::Result<String> {
let url = format!("https://discord.com/api/v10/channels/{recipient}/messages");
let body = payload.to_rest_json();
let resp = client
.post(&url)
.header("Authorization", format!("Bot {bot_token}"))
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let err = resp
.text()
.await
.unwrap_or_else(|e| format!("<failed to read response body: {e}>"));
anyhow::bail!("Discord send message failed ({status}): {err}");
}
extract_message_id(resp).await
}
pub(crate) async fn send_discord_outgoing(
client: &reqwest::Client,
bot_token: &str,
recipient: &str,
outgoing: &DiscordOutgoing,
) -> anyhow::Result<String> {
let url = format!("https://discord.com/api/v10/channels/{recipient}/messages");
let body = outgoing.to_rest_json();
let resp = client
.post(&url)
.header("Authorization", format!("Bot {bot_token}"))
.json(&body)View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the status and body in the error text first — it names the exact cause (401 token, 403 permissions, 50035 invalid form body)
- 401 → fix/rotate the bot token in config and restart the channel
- 403 → re-invite the bot or grant Send Messages + View Channel on the target channel
- 400 → shorten content below 2000 chars or fix the embed payload shape
- 429 → honor the Retry-After header and resend with backoff
Defensive patterns
Strategy: retry
Validate before calling
// Rust — cheap preflight before sending
fn discord_send_precheck(recipient: &str, content: &str) -> Result<(), String> {
let channel_id = recipient.split(':').next().unwrap_or(recipient);
if channel_id.parse::<u64>().is_err() {
return Err(format!("recipient '{recipient}' has no valid channel id"));
}
if content.chars().count() > 2000 {
return Err("content exceeds Discord's 2000-character limit".into());
}
Ok(())
} Try / catch
let mut attempt = 0;
loop {
match send_discord_message_payload(&client, &url, &payload).await {
Ok(id) => break Ok(id),
Err(e) if attempt < 3 && e.to_string().contains("429") => {
attempt += 1;
tokio::time::sleep(retry_after_delay(&e)).await; // honor Retry-After
}
Err(e) => break Err(e), // 4xx: fix token/permissions/payload, do not blind-retry
}
} Prevention
- A 401 here means every send is failing — validate the bot token at startup, not per message
- Pre-truncate content to 2000 characters before sending
- Serialize per-channel send queues so bursts do not trip 429s
When it happens
Trigger: Any non-success status from the message POST: 401 invalid/expired bot token, 403 missing Send Messages permission or no access to the channel id, 400 invalid form body (content over 2000 chars, malformed embed), 429 rate limit.
Common situations: Bot token regenerated after the channel started; wrong or stale channel id in the recipient; channel permission changes after a role reshuffle; message content exceeding Discord limits; bursting sends into rate limits.
Related errors
- channel does not support room creation
- interaction followup post failed ({status}): {err}
- Discord gate-prompt finalize failed ({status})
- Discord send message with files failed ({status}): {err}
- delete message failed ({status}): {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/bc8013b50e0b21cb.
Report an issue: GitHub.