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

interaction followup post failed ({status}): {err}

Error message

interaction followup post failed ({status}): {err}

What it means

discord_post_interaction_followup POSTs content chunks beyond the first (replies over Discord's 2000-char limit) to {api_base}/webhooks/{app_id}/{token}. Non-2xx becomes this error. Beyond the shared token-expiry mode, this endpoint is the one most exposed to Discord rate limits (429) during multi-chunk bursts and to 50035 when a chunk itself exceeds 2000 chars after marker expansion.

Source

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

/// chunks beyond the first when a reply exceeds Discord's 2000-char limit.
pub(crate) async fn discord_post_interaction_followup(
    client: &reqwest::Client,
    app_id: &str,
    interaction_token: &str,
    api_base: &str,
    content: &str,
) -> anyhow::Result<()> {
    let url = format!("{api_base}/webhooks/{app_id}/{interaction_token}");
    let resp = client
        .post(&url)
        .json(&DiscordOutgoing::text(content).to_rest_json())
        .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 followup post failed ({status}): {err}");
    }
    Ok(())
}

#[cfg(test)]
mod embed_reply_tests {
    use super::*;
    use wiremock::matchers::{body_json, body_partial_json, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[tokio::test]
    async fn slash_reply_attaches_embeds_to_the_original_edit() {
        let server = MockServer::start().await;
        Mock::given(method("PATCH"))
            .and(path("/webhooks/app/tok/messages/@original"))
            .and(body_partial_json(serde_json::json!({
                "content": "see below",
                "embeds": [{ "title": "Report" }]

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Honor 429: read retry_after from the response and delay before sending remaining chunks
  2. Re-chunk to ≤2000 chars after marker expansion, not before
  3. Deliver all chunks within the 15-minute token window — summarize if tools run long
  4. On 404 stop the loop: remaining chunks cannot reach that token

Example fix

// before: fire all chunks back-to-back
for chunk in chunks {
    discord_post_interaction_followup(&client, app, tok, base, chunk).await?;
}

// after: honor rate limits
for chunk in chunks {
    if let Err(e) = discord_post_interaction_followup(&client, app, tok, base, chunk).await {
        if let Some(d) = parse_retry_after(&e) { tokio::time::sleep(d).await; continue; }
        break;
    }
}
Defensive patterns

Strategy: retry

Try / catch

for chunk in chunks {
    if let Err(e) = discord_post_interaction_followup(&client, app, tok, base, chunk).await {
        if let Some(d) = parse_retry_after(&e) {
            tokio::time::sleep(d).await;
            continue; // honor 429 Retry-After, then resend this chunk
        }
        break; // 404/expired token — remaining chunks are undeliverable
    }
}

Prevention

When it happens

Trigger: Several followup posts in quick succession hitting per-channel rate limits without honoring Retry-After; a chunk over 2000 chars after embed/component markers are expanded; token older than 15 minutes by the time later chunks send.

Common situations: Verbose agent outputs with large tool logs; chunk sends without inter-chunk delay; late chunks after a long-running tool call crosses the TTL.

Related errors


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