{"record":{"id":"94374ff343de0479","repo":"zeroclaw-labs/zeroclaw","slug":"discord-remove-reaction-failed-status-err","errorCode":null,"errorMessage":"Discord remove reaction failed ({status}): {err}","messagePattern":"Discord remove reaction failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/discord/mod.rs","lineNumber":3832,"sourceCode":"        if parse_discord_interaction_target(channel_id).is_some() {\n            return Ok(());\n        }\n        let url = discord_reaction_url(channel_id, message_id, emoji);\n\n        let resp = self\n            .http_client()\n            .delete(&url)\n            .header(\"Authorization\", format!(\"Bot {}\", self.bot_token))\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 remove reaction failed ({status}): {err}\");\n        }\n\n        Ok(())\n    }\n\n    /// Delegates to [`Self::request_approval_attributed`] and drops the\n    /// provenance, so the prompt/timeout logic lives in exactly one place.\n    async fn request_approval(\n        &self,\n        recipient: &str,\n        request: &ChannelApprovalRequest,\n    ) -> anyhow::Result<Option<ChannelApprovalResponse>> {\n        Ok(self\n            .request_approval_attributed(recipient, request)\n            .await?\n            .map(|attributed| attributed.response))\n    }\n","sourceCodeStart":3814,"sourceCodeEnd":3850,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/discord/mod.rs#L3814-L3850","documentation":"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.","triggerScenarios":"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.","commonSituations":"Idempotent cleanup code hitting double-removal; streamed messages deleted mid-flow; restarts replaying cleanup against stale state.","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"],"exampleFix":"// before: any failure is an error\nchannel.remove_reaction(ch, msg_id, emoji).await?;\n\n// after: already-removed / gone is fine\nif let Err(e) = channel.remove_reaction(ch, msg_id, emoji).await {\n    if !e.to_string().contains(\"10008\") && !e.to_string().contains(\"404\") {\n        tracing::warn!(\"remove reaction failed: {e}\");\n    }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(e) = channel.remove_reaction(ch, msg_id, emoji).await {\n    let s = e.to_string();\n    if !s.contains(\"10008\") && !s.contains(\"404\") {\n        tracing::warn!(\"remove reaction failed: {s}\"); // already-removed is success\n    }\n}","preventionTips":["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"],"tags":["discord","reactions","cleanup","idempotency"],"backgroundTag":"discord-reaction-errors","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}