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
- Pass an E.164 phone number (`+15551234567` or `15551234567`) or a full JID such as `15551234567@s.whatsapp.net`.
- Map usernames/IDs to phone numbers before calling the channel.
- 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
- Store and pass recipients as E.164 (`+15551234567`) or full JIDs end to end.
- Map usernames/IDs to phone numbers in your own directory before invoking the channel.
- Add a recipient lint check to your test fixtures so name-keyed contacts fail fast in CI.
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
- Recipient cannot be empty
- TTS returned empty audio
- WhatsApp marker target {} is empty
- matrix: not a room id or alias: {id_or_alias}
- WhatsApp interactive buttons require 1..=3 options (got {});
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9bbd369f0964fc06.
Report an issue: GitHub.