zeroclaw-labs/zeroclaw · warning · anyhow::Error

interaction reject failed ({status}): {err}

Error message

interaction reject failed ({status}): {err}

What it means

discord_reject_interaction answers a policy-refused interaction immediately with a type-4 ephemeral message (flags 64) so the invoker sees why, instead of Discord's generic "The application did not respond". Non-2xx becomes this error. It fails on the same grounds as other initial callbacks: token expired/unknown (handler slower than the ~3s ack window), interaction already acknowledged (e.g. a defer also fired), or invalid body such as content over 2000 chars.

Source

Thrown at crates/zeroclaw-channels/src/discord/interaction.rs:163

    );
    let body = json!({
        "type": 4,
        "data": {
            "content": message,
            "flags": 64
        }
    });
    // without_url: transport errors embed the token-bearing URL.
    let resp = client
        .post(&url)
        .json(&body)
        .send()
        .await
        .map_err(reqwest::Error::without_url)?;
    if !resp.status().is_success() {
        let status = resp.status();
        let err = resp.text().await.unwrap_or_default();
        anyhow::bail!("interaction reject failed ({status}): {err}");
    }
    Ok(())
}

pub(crate) async fn discord_edit_interaction_response(
    client: &reqwest::Client,
    app_id: &str,
    interaction_token: &str,
    api_base: &str,
    content: &str,
    embeds: &[DiscordEmbed],
    components: &[DiscordActionRow],
) -> anyhow::Result<()> {
    let url = format!("{api_base}/webhooks/{app_id}/{interaction_token}/messages/@original");
    // No truncation: the caller chunks (deliver_interaction_answer) and this edit
    // carries the first ≤2000-char chunk plus any embeds (EPIC C) and any
    // interactive action rows (EPIC B). `to_rest_json` omits whichever are empty,
    // so a plain text reply stays byte-identical.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Reject fast: authorization decisions should be local and immediate
  2. Guard against double-ack — exactly one of defer/reject/modal per interaction
  3. Keep the rejection message under 2000 chars
  4. On 404/expired, drop the turn: the user already saw the timeout state

Example fix

// before: remote check, then reject (may miss the 3s window)
let ok = remote_acl(user).await?;
if !ok { discord_reject_interaction(&client, &id, &token, msg).await?; }

// after: local gate, immediate ephemeral rejection
if !local_gate(user) {
    discord_reject_interaction(&client, &id, &token, msg).await?;
    return Ok(());
}
Defensive patterns

Strategy: try-catch

Try / catch

if discord_reject_interaction(&client, &id, &token, reason).await.is_err() {
    // user already saw the timeout state — log and drop the turn
}

Prevention

When it happens

Trigger: Authorization or gate checks slow enough to miss the ack window; rejecting an interaction that was already deferred or answered; rejection message longer than 2000 chars; replaying old interactions.

Common situations: Allowlist checks hitting remote services before rejecting; double-ack bugs where defer and reject both fire; policy messages built from verbose templates.

Related errors


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