{"record":{"id":"efabb4ccde9dc14f","repo":"zeroclaw-labs/zeroclaw","slug":"modal-custom-id-exceeds-discord-s-100-char-limit","errorCode":null,"errorMessage":"modal custom_id exceeds Discord's 100-char limit; cannot open","messagePattern":"modal custom_id exceeds Discord's 100-char limit; cannot open","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/discord/interaction.rs","lineNumber":64,"sourceCode":"        let status = resp.status();\n        let err = resp.text().await.unwrap_or_default();\n        anyhow::bail!(\"interaction defer failed ({status}): {err}\");\n    }\n    Ok(())\n}\n\n/// Open a modal in response to a button/slash interaction (callback type 9).\n/// The caller registers the modal's `custom_id` in the pending registry as a\n/// resolve-into-turn so the eventual type-5 submit resolves it. Driven by the\n/// `OpenModal` dispatch arm (a `[COMPONENTS:…]` modal button click).\npub(crate) async fn discord_open_modal(\n    client: &reqwest::Client,\n    interaction_id: &str,\n    interaction_token: &str,\n    modal: &super::components::DiscordModal,\n) -> anyhow::Result<()> {\n    let Some(data) = modal.to_api() else {\n        anyhow::bail!(\"modal custom_id exceeds Discord's 100-char limit; cannot open\");\n    };\n    let url = format!(\n        \"https://discord.com/api/v10/interactions/{interaction_id}/{interaction_token}/callback\"\n    );\n    // type 9 = MODAL\n    let body = json!({ \"type\": 9, \"data\": data });\n    // without_url: transport errors embed the token-bearing URL.\n    let resp = client\n        .post(&url)\n        .json(&body)\n        .send()\n        .await\n        .map_err(reqwest::Error::without_url)?;\n    if !resp.status().is_success() {\n        let status = resp.status();\n        let err = resp.text().await.unwrap_or_default();\n        anyhow::bail!(\"modal open failed ({status}): {err}\");\n    }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/discord/interaction.rs#L46-L82","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: full payload embedded in the custom_id\nlet modal = DiscordModal { custom_id: CustomId::new(format!(\"zc1:edit:{draft}\")), .. };\n\n// after: short key, payload stored out-of-band\nlet key = store.insert(draft); // e.g. \"k3f9\"\nlet modal = DiscordModal { custom_id: CustomId::new(format!(\"zc1:edit:{key}\")), .. };","handlingStrategy":"validation","validationCode":"// to_api() is the authority: if it cannot encode, neither can Discord.\nif modal.to_api().is_none() {\n    // rebuild the custom_id with a shorter argument before attempting to open\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["discord","modal","components","custom-id","validation"],"backgroundTag":"discord-custom-id-limit","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}