{"record":{"id":"bc8013b50e0b21cb","repo":"zeroclaw-labs/zeroclaw","slug":"discord-send-message-failed-status-err","errorCode":null,"errorMessage":"Discord send message failed ({status}): {err}","messagePattern":"Discord send message failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/discord/rest.rs","lineNumber":55,"sourceCode":"    payload: &DiscordOutgoing,\n) -> anyhow::Result<String> {\n    let url = format!(\"https://discord.com/api/v10/channels/{recipient}/messages\");\n    let body = payload.to_rest_json();\n\n    let resp = client\n        .post(&url)\n        .header(\"Authorization\", format!(\"Bot {bot_token}\"))\n        .json(&body)\n        .send()\n        .await?;\n\n    if !resp.status().is_success() {\n        let status = resp.status();\n        let err = resp\n            .text()\n            .await\n            .unwrap_or_else(|e| format!(\"<failed to read response body: {e}>\"));\n        anyhow::bail!(\"Discord send message failed ({status}): {err}\");\n    }\n\n    extract_message_id(resp).await\n}\n\npub(crate) async fn send_discord_outgoing(\n    client: &reqwest::Client,\n    bot_token: &str,\n    recipient: &str,\n    outgoing: &DiscordOutgoing,\n) -> anyhow::Result<String> {\n    let url = format!(\"https://discord.com/api/v10/channels/{recipient}/messages\");\n    let body = outgoing.to_rest_json();\n\n    let resp = client\n        .post(&url)\n        .header(\"Authorization\", format!(\"Bot {bot_token}\"))\n        .json(&body)","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/discord/rest.rs#L37-L73","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Rust — cheap preflight before sending\nfn discord_send_precheck(recipient: &str, content: &str) -> Result<(), String> {\n    let channel_id = recipient.split(':').next().unwrap_or(recipient);\n    if channel_id.parse::<u64>().is_err() {\n        return Err(format!(\"recipient '{recipient}' has no valid channel id\"));\n    }\n    if content.chars().count() > 2000 {\n        return Err(\"content exceeds Discord's 2000-character limit\".into());\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"let mut attempt = 0;\nloop {\n    match send_discord_message_payload(&client, &url, &payload).await {\n        Ok(id) => break Ok(id),\n        Err(e) if attempt < 3 && e.to_string().contains(\"429\") => {\n            attempt += 1;\n            tokio::time::sleep(retry_after_delay(&e)).await; // honor Retry-After\n        }\n        Err(e) => break Err(e), // 4xx: fix token/permissions/payload, do not blind-retry\n    }\n}","preventionTips":["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"],"tags":["discord","rest","http-status","send-message","rate-limit"],"backgroundTag":"discord-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}