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

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

Error message

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

What it means

discord_defer_interaction POSTs a type-5 DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE callback to /interactions/{id}/{token}/callback to acknowledge within Discord's 3-second window. Non-2xx lands here with status and body. Classic causes: unknown or already-consumed interaction token (404/400) or a handler too slow to meet the window — the user then sees "The application did not respond". Transport errors are mapped through reqwest::Error::without_url so the token-bearing URL never leaks into logs.

Source

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

    interaction_token: &str,
) -> anyhow::Result<()> {
    let url = format!(
        "https://discord.com/api/v10/interactions/{interaction_id}/{interaction_token}/callback"
    );
    // type 5 = DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
    let body = json!({ "type": 5 });
    // without_url: reqwest transport errors embed the full request URL,
    // which here contains the interaction token (a live credential).
    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 defer failed ({status}): {err}");
    }
    Ok(())
}

/// Open a modal in response to a button/slash interaction (callback type 9).
/// The caller registers the modal's `custom_id` in the pending registry as a
/// resolve-into-turn so the eventual type-5 submit resolves it. Driven by the
/// `OpenModal` dispatch arm (a `[COMPONENTS:…]` modal button click).
pub(crate) async fn discord_open_modal(
    client: &reqwest::Client,
    interaction_id: &str,
    interaction_token: &str,
    modal: &super::components::DiscordModal,
) -> anyhow::Result<()> {
    let Some(data) = modal.to_api() else {
        anyhow::bail!("modal custom_id exceeds Discord's 100-char limit; cannot open");
    };
    let url = format!(

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Defer first, work second — move the defer to the very top of the interaction dispatch arm
  2. If the body says Unknown interaction or already acknowledged, do not re-defer; answer via the followup webhook or drop the turn
  3. Keep the pre-ack path free of awaits that can block (remote calls, lock contention)
  4. Confirm gateway events arrive fresh (healthy connection, no long resume gaps)

Example fix

// before: heavy work then ack
let answer = run_agent(prompt).await?;
discord_defer_interaction(&client, &id, &token).await?;

// after: ack inside the 3s window, then work
discord_defer_interaction(&client, &id, &token).await?;
let answer = run_agent(prompt).await?;
Defensive patterns

Strategy: try-catch

Try / catch

if let Err(e) = discord_defer_interaction(&client, &id, &token).await {
    // token consumed/expired: do NOT re-defer — answer via the followup webhook or drop the turn
    tracing::warn!("defer failed: {e}");
}

Prevention

When it happens

Trigger: Slow work before the defer exceeds ~3s from interaction creation; deferring the same interaction twice (only one initial response is allowed); id/token mismatch or events replayed after a gateway resume; dispatch arm blocked on locks or cold caches.

Common situations: Agent performing tool calls or slow LLM inference before acknowledging; startup jank delaying the first callback; replaying captured interaction payloads in dev.

Related errors


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