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

  1. Treat 404/10008 (already gone or message deleted) as success — removal is idempotent
  2. Throttle cleanup sweeps to avoid 429s
  3. Use the exact same emoji encoding for add and remove (share the discord_reaction_url inputs)
  4. 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

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


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