zeroclaw-labs/zeroclaw · error

WhatsApp interactive list requires at least one section

Error message

WhatsApp interactive list requires at least one section

What it means

`send_interactive_list` builds a WhatsApp interactive list message, and Meta's contract requires at least one section. The channel bails immediately when the `sections` slice is empty rather than sending a request that would fail server-side. This is a local precondition failure; no network call is made.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp.rs:612

        self.post_to_meta(&url, &body).await
    }

    /// Send an interactive list message (more than 3 options, up to 10
    /// rows per section, up to 10 sections per Meta's limits). `sections`
    /// each carry a section title + rows. Each row's `id` round-trips
    /// through the inbound `interactive.list_reply.id` as `[choice]<id>`.
    ///
    /// `button_text` is the button label that opens the list (Meta limits
    /// to 20 chars).
    pub async fn send_interactive_list(
        &self,
        recipient: &str,
        body_text: &str,
        button_text: &str,
        sections: &[InteractiveListSection],
    ) -> anyhow::Result<()> {
        if sections.is_empty() {
            anyhow::bail!("WhatsApp interactive list requires at least one section");
        }
        if sections.len() > 10 {
            anyhow::bail!(
                "WhatsApp interactive list capped at 10 sections (got {})",
                sections.len()
            );
        }
        for s in sections {
            if s.rows.len() > 10 {
                anyhow::bail!(
                    "WhatsApp interactive list section '{}' capped at 10 rows (got {})",
                    s.title,
                    s.rows.len()
                );
            }
        }
        let url = format!(
            "https://graph.facebook.com/v18.0/{}/messages",

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Guard the empty case upstream: with no options, send the prompt as plain text via `send` (or no-op when the prompt is also empty).
  2. Debug why the sections-producing collection was empty — usually the input list was empty or fully filtered.
  3. Prefer `send_choice`, which already handles empty options by falling back to a plain-text send.

Example fix

// before
channel
    .send_interactive_list(to, "Choose:", "Choose", &sections)
    .await?; // sections is empty -> bails

// after
if sections.is_empty() {
    channel.send(&SendMessage::new(prompt, to)).await?;
} else {
    channel.send_interactive_list(to, "Choose:", "Choose", &sections).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

if sections.is_empty() {
    anyhow::bail!("refusing to send an empty interactive list; check the option source");
}
channel.send_interactive_list(recipient, body, button_text, &sections).await

Type guard

fn has_list_sections(sections: &[InteractiveListSection]) -> bool {
    !sections.is_empty()
}

Prevention

When it happens

Trigger: Calling `send_interactive_list(recipient, body_text, button_text, &[])` — typically sections produced by a `chunks`/`map`/filter chain over an empty or fully filtered-out option collection.

Common situations: Grouping options into sections algorithmically (e.g. `options.chunks(10)`) when `options` itself is empty; an upstream data source returning zero rows; scaffolding code passing a placeholder empty vec.

Related errors


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