Summary
void send --via whatsapp prints Message sent (id: ...) and exits 0 for messages that are never delivered. The returned id is generated locally before transmission, and nothing waits for the server ack, so a send on a dead or dying socket is reported as a success.
This is a silent data-loss bug: the caller has no way to know the message did not leave the machine.
Reproduction (observed in production, 2026-09-11)
void send --via whatsapp --connection WA-french --to "<jid>@lid" --message "..."
Message sent (id: 3EB01E021E254E73BF2930)
Returned in 0.66 s. The message was never delivered. Verified three independent ways, 80 seconds later:
- absent from WhatsApp's own store
ChatStorage.sqlite, read via a WAL-inclusive copy of the .sqlite / -wal / -shm triple, so this is not a read-staleness artifact
- absent from void's own store:
void messages <conversation_id> did not list it
- no delivery receipt anywhere in the sync log
What the sync log shows:
18:44:25.092 INFO void_whatsapp::connector::ops: sending WhatsApp message via sync connection_id=WA-french recipient_jid=<jid>@lid
18:45:17.977 WARN wa_rs::client: Message loop exited with an error. Will attempt to reconnect if enabled.
18:45:17.977 WARN void_whatsapp::connector::connector_trait: WhatsApp disconnected, waiting for reconnect
For contrast, the same text sent a few minutes later through another path produced the signature of a real send in the same log:
[whatsapp:WA-french] JL - me: <text>
INFO wa_rs::receipt: Received receipt type 'Other("delivery")' for message 3BA4982CACB860074875 from <jid>:13@lid
A healthy send yields a server ack and later delivery receipts. The failed one yielded neither, yet the CLI reported success.
Root cause
The id is generated locally, before transmission, and returned unconditionally.
wa-rs 0.2.0, src/send.rs:40:
pub async fn send_message_with_options(&self, to: Jid, message: wa::Message, options: SendOptions)
-> Result<String, anyhow::Error> {
let request_id = self.generate_message_id().await;
self.send_message_impl(to, &message, Some(request_id.clone()), ...).await?;
Ok(request_id) // returned as soon as bytes are handed to the socket
}
send_message_impl ends with self.send_node(stanza_to_send).await, and send_node (src/client.rs:2014) only marshals, encrypts and writes to the noise socket. It returns Ok(()) once the bytes are written. Nobody waits for the server ack stanza. If the socket is dead, or dies mid-write (our case: the message loop exited about 50 s later), the local write succeeds while the stanza never reaches WhatsApp.
On void's side the call site is crates/void-whatsapp/src/connector/ops.rs, send_via_sync, which returns the id upward, and the CLI prints Message sent (id: ...).
Worth noting: the library already has the machinery to wait for a server response. client.response_waiters (src/client.rs:107) is populated and woken on incoming acks (src/client.rs:1313), and src/request.rs:135 shows the register / await / timeout pattern used for IQs. An ack-aware send is expressible with existing primitives.
Expected behaviour
- A send that is not acked by the server exits non-zero with a clear error (dead socket, disconnect during send, timeout). An "unknown" outcome is acceptable as long as the wording does not claim success.
- A bounded wait for the server ack, with a distinguishable outcome for timeout versus transport error.
- A connection-liveness precheck before sending, so a send on a disconnected or reconnecting connection fails fast instead of writing into the void.
- No regression on the happy path: a normal send still returns the id promptly.
Impact
Any automation built on void send inherits a false success signal. In our case a time-critical message was reported as sent and was not, and the failure was only caught because we cross-checked ChatStorage.sqlite and the sync log by hand. Anything that retries on failure will also never retry, since it never sees a failure.
Environment: void built from ccb65f6, wa-rs 0.2.0, macOS 26.6.2.
A PR follows.
Summary
void send --via whatsappprintsMessage sent (id: ...)and exits 0 for messages that are never delivered. The returned id is generated locally before transmission, and nothing waits for the server ack, so a send on a dead or dying socket is reported as a success.This is a silent data-loss bug: the caller has no way to know the message did not leave the machine.
Reproduction (observed in production, 2026-09-11)
Returned in 0.66 s. The message was never delivered. Verified three independent ways, 80 seconds later:
ChatStorage.sqlite, read via a WAL-inclusive copy of the.sqlite/-wal/-shmtriple, so this is not a read-staleness artifactvoid messages <conversation_id>did not list itWhat the sync log shows:
For contrast, the same text sent a few minutes later through another path produced the signature of a real send in the same log:
A healthy send yields a server ack and later delivery receipts. The failed one yielded neither, yet the CLI reported success.
Root cause
The id is generated locally, before transmission, and returned unconditionally.
wa-rs 0.2.0,src/send.rs:40:send_message_implends withself.send_node(stanza_to_send).await, andsend_node(src/client.rs:2014) only marshals, encrypts and writes to the noise socket. It returnsOk(())once the bytes are written. Nobody waits for the server ack stanza. If the socket is dead, or dies mid-write (our case: the message loop exited about 50 s later), the local write succeeds while the stanza never reaches WhatsApp.On void's side the call site is
crates/void-whatsapp/src/connector/ops.rs,send_via_sync, which returns the id upward, and the CLI printsMessage sent (id: ...).Worth noting: the library already has the machinery to wait for a server response.
client.response_waiters(src/client.rs:107) is populated and woken on incoming acks (src/client.rs:1313), andsrc/request.rs:135shows the register / await / timeout pattern used for IQs. An ack-aware send is expressible with existing primitives.Expected behaviour
Impact
Any automation built on
void sendinherits a false success signal. In our case a time-critical message was reported as sent and was not, and the failure was only caught because we cross-checkedChatStorage.sqliteand the sync log by hand. Anything that retries on failure will also never retry, since it never sees a failure.Environment: void built from
ccb65f6,wa-rs0.2.0, macOS 26.6.2.A PR follows.