zeroclaw-labs/zeroclaw · error

Recipient `{trimmed}` does not contain a valid phone number

Error message

Recipient `{trimmed}` does not contain a valid phone number

What it means

For recipients without `@` (i.e. not already a JID), `recipient_to_jid` extracts the ASCII digits and treats them as a phone-number JID; if the trimmed string contains no digits at all, it bails and echoes the offending string. Strings containing `@` take a different path and fail with `Invalid WhatsApp JID ...` instead.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp_web.rs:1201

        if trimmed.contains('@') {
            return trimmed.parse::<wacore_binary::jid::Jid>().map_err(|e| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({
                            "trimmed": trimmed,
                            "error": format!("{}", e),
                        })),
                    "whatsapp_web: invalid JID"
                );
                anyhow::Error::msg(format!("Invalid WhatsApp JID `{trimmed}`: {e}"))
            });
        }

        let digits: String = trimmed.chars().filter(|c| c.is_ascii_digit()).collect();
        if digits.is_empty() {
            anyhow::bail!("Recipient `{trimmed}` does not contain a valid phone number");
        }

        Ok(wacore_binary::jid::Jid::pn(digits))
    }

    // ── Reconnect state-machine helpers (used by listen() and tested directly) ──

    /// Reconnect retry constants.
    const MAX_RETRIES: u32 = 10;
    const BASE_DELAY_SECS: u64 = 3;
    const MAX_DELAY_SECS: u64 = 300;

    /// Compute the exponential-backoff delay for a given 1-based attempt number.
    /// Doubles each attempt from `BASE_DELAY_SECS`, capped at `MAX_DELAY_SECS`.
    fn compute_retry_delay(attempt: u32) -> u64 {
        std::cmp::min(
            Self::BASE_DELAY_SECS.saturating_mul(2u64.saturating_pow(attempt.saturating_sub(1))),
            Self::MAX_DELAY_SECS,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Pass an E.164 phone number (`+15551234567` or `15551234567`) or a full JID such as `15551234567@s.whatsapp.net`.
  2. Map usernames/IDs to phone numbers before calling the channel.
  3. Validate recipients at the edge of your pipeline: non-empty, and either contains `@` (JID) or at least one digit.

Example fix

// before
channel.send(&SendMessage::new(text, "alice")).await?; // no digits -> bails

// after
channel.send(&SendMessage::new(text, "+15551234567")).await?;
// or a full JID:
channel.send(&SendMessage::new(text, "15551234567@s.whatsapp.net")).await?;
Defensive patterns

Strategy: validation

Validate before calling

let r = recipient.trim();
anyhow::ensure!(
    !r.is_empty() && (r.contains('@') || r.chars().any(|c| c.is_ascii_digit())),
    "recipient must be a phone number or JID, got `{r}`"
);
channel.send(&SendMessage::new(text, r)).await

Type guard

fn is_sendable_recipient(recipient: &str) -> bool {
    let r = recipient.trim();
    !r.is_empty() && (r.contains('@') || r.chars().any(|c| c.is_ascii_digit()))
}

Prevention

When it happens

Trigger: A send whose recipient is alphabetic or punctuation-only with no ASCII digits: `"alice"`, `"+"`, `"---"`, `"support_team"`.

Common situations: Passing usernames, display names, or channel-internal IDs where a phone number or JID is expected; contact records keyed by name rather than number; email-like strings without a domain.

Related errors


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