zeroclaw-labs/zeroclaw · error · anyhow::Error
modal custom_id exceeds Discord's 100-char limit; cannot ope
Error message
modal custom_id exceeds Discord's 100-char limit; cannot open
What it means
discord_open_modal serializes the DiscordModal via to_api(), which returns None when the modal's routing custom_id (the zc1 token) cannot be encoded within Discord's hard 100-character custom_id limit. This bail fires before any HTTP call: the payload is unrepresentable, so the library refuses locally. Unlike label (45 chars) and value (4000 chars), which to_api clamps, the custom_id cannot be truncated without breaking submit routing — hence the hard failure.
Source
Thrown at crates/zeroclaw-channels/src/discord/interaction.rs:64
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!(
"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}");
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Move the payload out-of-band: store it under a short key and embed only the key in custom_id
- Shorten the routing token deterministically (hash or truncate the argument)
- Validate at construction time so oversized ids never reach open_modal (see validationCode)
- Pin the max encoded id length with a unit test over your longest realistic input
Example fix
// before: full payload embedded in the custom_id
let modal = DiscordModal { custom_id: CustomId::new(format!("zc1:edit:{draft}")), .. };
// after: short key, payload stored out-of-band
let key = store.insert(draft); // e.g. "k3f9"
let modal = DiscordModal { custom_id: CustomId::new(format!("zc1:edit:{key}")), .. }; Defensive patterns
Strategy: validation
Validate before calling
// to_api() is the authority: if it cannot encode, neither can Discord.
if modal.to_api().is_none() {
// rebuild the custom_id with a shorter argument before attempting to open
} Prevention
- Never embed unbounded data in custom_id — 100 chars is a hard Discord limit
- Route by a short key and keep payloads in a store
- Test modal ids at their longest realistic input
When it happens
Trigger: A modal custom_id embedding a long argument (draft body, file path, serialized payload) that pushes the encoded id over 100 chars; OpenModal dispatch driven by an oversized [COMPONENTS:...] marker; ids built from unbounded user-supplied strings.
Common situations: Edit-modals that stuff the current draft into the custom_id; tool outputs or long paths used as routing args; payloads that grow over time and cross 100 chars only for some inputs.
Related errors
- modal open failed ({status}): {err}
- slash command registration failed for '{name}' ({status}): {
- interaction defer failed ({status}): {err}
- interaction autocomplete answer failed ({status}): {err}
- interaction reject failed ({status}): {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/efabb4ccde9dc14f.
Report an issue: GitHub.