Overview
The transport layer provides a runtime-agnostic abstraction for network connections. It handles raw byte transmission without knowledge of WhatsApp’s protocol framing. The transport system consists of two main traits:- Transport - Represents an active connection for sending/receiving raw bytes
- TransportFactory - Creates new transport instances and event streams
Transport Trait
TheTransport trait represents an active network connection as a simple byte pipe.
MaybeSendSync is Send + Sync on native targets and carries no bounds on wasm32. This means Arc<dyn Transport> remains thread-safe on native, while implementations backed by !Send JS handles (e.g. a browser WebSocket) compile on the wasm port without wrapping.Methods
send
Sends raw bytes through the transport. The caller is responsible for any protocol framing.data- Raw bytes to send
Ok(())on successErr(anyhow::Error)on failure
disconnect
Gracefully closes the connection.resource_report
Best-effort per-session footprint of this transport: read/write framing buffers plus a TLS/noise session-state estimate. Defaulted toNone (“not reported”) — a transport that can introspect its buffers overrides it.
Some(TransportResourceReport)with any subset ofread_buffer_bytes,write_buffer_bytes,tls_state_bytesfilled in (eachOption<u64>)None(default) if the transport doesn’t report
Client::resource_report(). The bundled Tokio WebSocket transport (below) overrides this with static estimates, since tokio-websockets and rustls don’t expose their live buffer sizes.
TransportFactory Trait
Creates new transport instances and associated event streams.Like
Transport, TransportFactory uses MaybeSendSync so that factory implementations holding !Send JS state compile on wasm32. On native the bound is Send + Sync as before.Methods
create_transport
Establishes a new connection and returns both the transport handle and an event receiver.Arc<dyn Transport>- The transport instance for sending dataasync_channel::Receiver<TransportEvent>- Stream of transport events
TransportEvent
Events produced by the transport layer:Since v0.6
Disconnected carries a DisconnectReason instead of being a unit variant (a breaking change for custom transports — update your match arms and the value you emit). It surfaces why the socket closed in logs, so server-initiated closes can be told apart from local shutdowns.Event Types
Connected
Emitted immediately after successful connection establishment.DataReceived
Emitted when raw data is received from the server.bytes: Bytes- Raw data received (from thebytescrate)
Disconnected
Emitted when the connection is closed (gracefully or due to error). The attachedDisconnectReason says why:
DisconnectReason implements Display for human-readable logging. Custom transports should emit the most specific variant they can determine.
is_clean_shutdown() tells a benign, server-initiated stream recycle (the normal WhatsApp reconnect path) apart from a genuine transport failure, so consumers of events::Disconnected don’t have to parse logs to classify a disconnect. It’s deliberately conservative — anything ambiguous returns false (treated as a real failure):
Tokio WebSocket transport
The default transport implementation usingtokio-websockets for async WebSocket connections.
Features
- Async I/O - Built on Tokio runtime
- TLS support - Uses rustls with webpki-roots for certificate validation
- Split architecture - Separate read/write paths for efficiency
- Generic over streams - Works with any
AsyncRead + AsyncWritestream type - Automatic reconnection - Handled by higher-level Client code
- Development mode - Optional
danger-skip-tls-verifyfeature
TokioWebSocketTransportFactory
The default factory handles DNS resolution, TCP connection, and TLS. For custom connection logic, usefrom_websocket directly.
When no custom connector is supplied via with_connector, the factory builds default_tls_connector() once, on the first call to create_transport, and retains it in a OnceLock for every subsequent dial on that factory — a reconnect no longer pays for a fresh TLS config. Retaining the connector also lets a later dial reuse resumption tickets an earlier handshake received: previously (the pre-#1245 behavior), rebuilding the config per dial discarded the resumption store — tickets and all — before the next dial could ever use it. A connector supplied explicitly via with_connector is used as-is and never touches this cache.
with_connector
Connector instead of the built-in default. This is the primary extension point for custom TLS configuration — for example, adding custom CA certificates or client certificates.
For full proxy support, implement TransportFactory directly and use from_websocket instead.
Parameters:
connector- Atokio_websockets::Connector(re-exported aswhatsapp_rust::transport::Connector)
Use
default_tls_connector() to inspect or replicate the default TLS configuration as a starting point before customizing. A connector returned by that function already carries the single-host resumption sizing described below, even when supplied via with_connector. A connector built independently with Connector::Rustls — as in the example above — does not get that sizing unless you apply it yourself.with_origin
Origin header on the WebSocket upgrade request. The default is WHATSAPP_WEB_ORIGIN ("https://web.whatsapp.com"), and it stays correct even when with_url points at a relay or a mock — a relay that simply forwards traffic to WhatsApp should still see the same Origin a real WA Web browser would send, regardless of which host you dial. Override it only when the peer you’re connecting to — the relay itself, not WhatsApp — validates Origin against its own value.
Parameters:
origin- The value to send as theOriginheader
without_origin
Origin header at all — this crate’s behavior before the header was added. Use it only for a peer that rejects the upgrade because of the header; no known WhatsApp endpoint does.
default_tls_connector
TokioWebSocketTransportFactory. Uses rustls with webpki_roots for certificate validation and ring as the crypto provider.
This is useful as a starting point when you need to inspect or replicate the default TLS configuration before customizing it via with_connector.
Its session-resumption store is sized for the one host a factory dials: 8 tickets, rustls’s per-server maximum, rather than the ~32-server table (⌈256/8⌉ slots) rustls’s Resumption::default() provisions for. A connector reused across several hosts (for example, shared deliberately across factories) still works, but only the most recently dialled hosts keep their tickets; build a larger rustls::client::Resumption store yourself if that’s the shape you need. TokioWebSocketTransportFactory itself only ever dials the one URL it was built with, so the default sizing is a straight win there — see TokioWebSocketTransportFactory above for how the resulting connector is cached and reused across reconnects.
Usage with Bot builder
from_websocket
Wraps an already-upgradedWebSocketStream into a Transport + event channel. This is useful when you need custom connection strategies such as IPv4 preference, TCP keepalive tuning, or connecting through a proxy.
ws- An already-connectedWebSocketStreamover any async stream type
Arc<dyn Transport>- The transport instance for sending dataasync_channel::Receiver<TransportEvent>- Stream of transport events
Connected event before the read pump starts — ensuring it always precedes any DataReceived events.
Example — IPv4-only connection with TCP keepalive:
TokioWebSocketTransportFactory itself delegates to from_websocket internally. Use the factory when the default DNS/TCP/TLS behavior is sufficient, and from_websocket when you need full control over the connection.Proxy support
The transport layer provides two approaches for proxy support, depending on your needs: Option 1: ImplementTransportFactory directly — for full control over the connection (SOCKS5, HTTP CONNECT, etc.). Establish a WebSocket through the proxy yourself, then wrap it with from_websocket:
with_connector — for custom TLS only (no proxy routing). The factory still handles DNS and TCP, but you control the TLS layer:
TLS configuration
By default, the transport validates TLS certificates using webpki-roots:TokioWebSocketTransportFactory only ever dials the one host it was built with. See default_tls_connector for the sizing rationale.
Development mode (skip TLS verification)
Enable thedanger-skip-tls-verify feature to disable certificate verification:
Connection settings
The default WebSocket URL is:TokioWebSocketTransportFactory also carries an Origin header by default:
Origin on the WebSocket handshake, and both whatsmeow and Baileys send this exact value unconditionally, regardless of the platform their ClientPayload claims. Override it with with_origin, or drop it entirely with without_origin for a peer that rejects the header.
The
Client wraps create_transport() in a 20-second timeout matching WhatsApp Web’s connect timeout. The transport itself does not enforce this timeout — it is applied at the client layer. If you use a custom transport, be aware that the client will abort the connection attempt after 20 seconds regardless of the transport’s own timeout behavior.WsTransport resource report
WsTransport overrides Transport::resource_report with static, documented estimates rather than a live measurement, since tokio-websockets and rustls don’t surface their buffer sizes: read_buffer_bytes: 16 KiB, write_buffer_bytes: 16 KiB, tls_state_bytes: 32 KiB (record buffers + key schedule for one TLS session). These give a realistic order-of-magnitude for the transport’s per-session contribution — tens of KiB — rather than an exact figure.
Internal architecture
The transport is generic over anyAsyncRead + AsyncWrite stream, split into separate read/write paths:
tokio::select! with a shutdown watch channel to ensure clean termination:
Implementing custom transports
You can implement custom transports for different runtimes or protocols.Example: mock transport for testing
Example: TCP transport (no TLS)
Best Practices
- Thread Safety - On native, implementations must be
Send + Sync(enforced viaMaybeSendSync). Onwasm32,MaybeSendSynccarries no bounds, so!Sendimplementations backed by JS handles compile. - Error Handling - Return descriptive errors from
send() - Graceful Shutdown - Implement proper cleanup in
disconnect() - Event Channel Size - Use bounded channels with reasonable capacity
- Read Task - Spawn a separate task for receiving data
- Resource Cleanup - Ensure sockets/resources are closed on drop
Testing Transports
Unit test example
See Also
- Storage Traits - Storage backend abstraction
- HTTP Client Trait - HTTP client abstraction
- Client API - Main client interface