zeroclaw-labs/zeroclaw · warning · anyhow::Error
Discord remove reaction failed ({status}): {err}
Error message
Discord remove reaction failed ({status}): {err} What it means
remove_reaction DELETEs the bot's own reaction at /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me. Non-2xx becomes this error with the body. Frequent causes: the reaction is already gone (double removal or a concurrent sweep), the message was deleted (10008), 403 Missing Permissions, or 429 during reaction-heavy cleanup. As with add_reaction, interaction sentinel ids are filtered before the call.
Source
Thrown at crates/zeroclaw-channels/src/discord/mod.rs:3832
if parse_discord_interaction_target(channel_id).is_some() {
return Ok(());
}
let url = discord_reaction_url(channel_id, message_id, emoji);
let resp = self
.http_client()
.delete(&url)
.header("Authorization", format!("Bot {}", self.bot_token))
.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 remove reaction failed ({status}): {err}");
}
Ok(())
}
/// Delegates to [`Self::request_approval_attributed`] and drops the
/// provenance, so the prompt/timeout logic lives in exactly one place.
async fn request_approval(
&self,
recipient: &str,
request: &ChannelApprovalRequest,
) -> anyhow::Result<Option<ChannelApprovalResponse>> {
Ok(self
.request_approval_attributed(recipient, request)
.await?
.map(|attributed| attributed.response))
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Treat 404/10008 (already gone or message deleted) as success — removal is idempotent
- Throttle cleanup sweeps to avoid 429s
- Use the exact same emoji encoding for add and remove (share the discord_reaction_url inputs)
- On 50001, grant permissions only if removing others' reactions is actually required
Example fix
// before: any failure is an error
channel.remove_reaction(ch, msg_id, emoji).await?;
// after: already-removed / gone is fine
if let Err(e) = channel.remove_reaction(ch, msg_id, emoji).await {
if !e.to_string().contains("10008") && !e.to_string().contains("404") {
tracing::warn!("remove reaction failed: {e}");
}
} Defensive patterns
Strategy: try-catch
Try / catch
if let Err(e) = channel.remove_reaction(ch, msg_id, emoji).await {
let s = e.to_string();
if !s.contains("10008") && !s.contains("404") {
tracing::warn!("remove reaction failed: {s}"); // already-removed is success
}
} Prevention
- Treat reaction removal as idempotent — 404/10008 is success
- Share the same emoji encoding between add and remove paths
- Throttle cleanup sweeps to stay under reaction rate limits
When it happens
Trigger: Cancel_draft/cleanup removing a reaction a concurrent sweep already removed; message deleted before cleanup runs; emoji key mismatch between add and remove (skin-tone/variation-selector differences); rate-limited cleanup loops.
Common situations: Idempotent cleanup code hitting double-removal; streamed messages deleted mid-flow; restarts replaying cleanup against stale state.
Related errors
- Discord add reaction failed ({status}): {err}
- {failed_deletes} stale skill command delete(s) failed; recon
- AcpChannel does not support reactions
- interaction defer failed ({status}): {err}
- modal custom_id exceeds Discord's 100-char limit; cannot ope
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/94374ff343de0479.
Report an issue: GitHub.