zeroclaw-labs/zeroclaw · error · anyhow::Error
modal open failed ({status}): {err}
Error message
modal open failed ({status}): {err} What it means
After the local custom_id check passes, discord_open_modal POSTs the type-9 MODAL callback to /interactions/{id}/{token}/callback. Non-2xx becomes this error with the body. A modal is an initial response: it fails when the interaction was already acknowledged (e.g. after a defer), when the token is expired/unknown, or when the modal data violates Discord validation (rows/components structure; note the library pre-truncates to 5 rows and clamps labels/values, so those are unlikely causes here).
Source
Thrown at crates/zeroclaw-channels/src/discord/interaction.rs:81
let Some(data) = modal.to_api() else {
anyhow::bail!("modal custom_id exceeds Discord's 100-char limit; cannot open");
};
let url = format!(
"https://discord.com/api/v10/interactions/{interaction_id}/{interaction_token}/callback"
);
// type 9 = MODAL
let body = json!({ "type": 9, "data": data });
// 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!("modal open failed ({status}): {err}");
}
Ok(())
}
pub(crate) async fn discord_answer_autocomplete(
client: &reqwest::Client,
interaction_id: &str,
interaction_token: &str,
choices: &[(String, String)],
) -> anyhow::Result<()> {
let url = format!(
"https://discord.com/api/v10/interactions/{interaction_id}/{interaction_token}/callback"
);
let rendered: Vec<serde_json::Value> = choices
.iter()
.take(25)
.map(|(name, value)| json!({ "name": name, "value": value }))
.collect();View on GitHub (pinned to 88bb9c8533)
Solutions
- Pick exactly one initial response per interaction: modal XOR defer — never both
- Open the modal immediately on the button/slash event, before await-heavy work
- Read the body: 'interaction has already been acknowledged' vs 'Unknown interaction' point to different fixes
- Re-verify modal shape against Discord limits (max 5 rows, one text input per row, label ≤45)
Example fix
// before: defer then try to open a modal discord_defer_interaction(&client, &id, &token).await?; discord_open_modal(&client, &id, &token, &modal).await?; // 400s // after: the modal is the sole initial response discord_open_modal(&client, &id, &token, &modal).await?;
Defensive patterns
Strategy: try-catch
Try / catch
if let Err(e) = discord_open_modal(&client, &id, &token, &modal).await {
// fall back to an ephemeral type-4 message so the user still gets feedback
let _ = discord_reject_interaction(&client, &id, &token, "Could not open form").await;
} Prevention
- Never open a modal after deferring the same interaction
- Open modals immediately on the triggering event
- Log the body — 'already acknowledged' vs 'Unknown interaction' need different fixes
When it happens
Trigger: Calling open_modal after defer_interaction on the same interaction (400 already acknowledged); token expired or unknown (404) from slow pre-work; malformed component JSON in the modal data; stale interactions after a gateway resume.
Common situations: Flow that defers "for safety" then decides to open a modal; policy checks running slow between receipt and open; component schema changes after a Discord API version bump.
Related errors
- modal custom_id exceeds Discord's 100-char limit; cannot ope
- interaction defer failed ({status}): {err}
- interaction autocomplete answer failed ({status}): {err}
- interaction reject failed ({status}): {err}
- interaction followup edit failed ({status}): {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e334d847873f6e52.
Report an issue: GitHub.