Skip to main content

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

The Transport 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.
Parameters:
  • data - Raw bytes to send
Returns:
  • Ok(()) on success
  • Err(anyhow::Error) on failure
Example:

disconnect

Gracefully closes the connection.
Example:

resource_report

Best-effort per-session footprint of this transport: read/write framing buffers plus a TLS/noise session-state estimate. Defaulted to None (“not reported”) — a transport that can introspect its buffers overrides it.
Returns:
  • Some(TransportResourceReport) with any subset of read_buffer_bytes, write_buffer_bytes, tls_state_bytes filled in (each Option<u64>)
  • None (default) if the transport doesn’t report
Feeds into 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.
Returns:
  • Arc<dyn Transport> - The transport instance for sending data
  • async_channel::Receiver<TransportEvent> - Stream of transport events
Example:

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.
Fields:
  • bytes: Bytes - Raw data received (from the bytes crate)

Disconnected

Emitted when the connection is closed (gracefully or due to error). The attached DisconnectReason 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 using tokio-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 + AsyncWrite stream type
  • Automatic reconnection - Handled by higher-level Client code
  • Development mode - Optional danger-skip-tls-verify feature

TokioWebSocketTransportFactory

The default factory handles DNS resolution, TCP connection, and TLS. For custom connection logic, use from_websocket directly.

with_connector

Uses a custom TLS 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 - A tokio_websockets::Connector (re-exported as whatsapp_rust::transport::Connector)
Example — custom CA certificate:
Use default_tls_connector() to inspect or replicate the default TLS configuration as a starting point before customizing.

default_tls_connector

Returns the default TLS connector used by 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.

Usage with Bot builder

from_websocket

Wraps an already-upgraded WebSocketStream 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.
Parameters:
  • ws - An already-connected WebSocketStream over any async stream type
Returns:
  • Arc<dyn Transport> - The transport instance for sending data
  • async_channel::Receiver<TransportEvent> - Stream of transport events
The function splits the WebSocket into read/write halves, spawns a background read pump task, and synchronously enqueues a Connected event before the read pump starts — ensuring it always precedes any DataReceived events. Example — IPv4-only connection with TCP keepalive:
Example — using with a custom TransportFactory:
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: Implement TransportFactory directly — for full control over the connection (SOCKS5, HTTP CONNECT, etc.). Establish a WebSocket through the proxy yourself, then wrap it with from_websocket:
Option 2: Use with_connector — for custom TLS only (no proxy routing). The factory still handles DNS and TCP, but you control the TLS layer:
See custom backends — proxy and custom TLS for a complete guide with examples.

TLS configuration

By default, the transport validates TLS certificates using webpki-roots:

Development mode (skip TLS verification)

Only use for development and testing.
Enable the danger-skip-tls-verify feature to disable certificate verification:
This allows connecting through MITM proxies or self-signed certificates.

Connection settings

The default WebSocket URL is:
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 any AsyncRead + AsyncWrite stream, split into separate read/write paths:
The read pump uses 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

  1. Thread Safety - On native, implementations must be Send + Sync (enforced via MaybeSendSync). On wasm32, MaybeSendSync carries no bounds, so !Send implementations backed by JS handles compile.
  2. Error Handling - Return descriptive errors from send()
  3. Graceful Shutdown - Implement proper cleanup in disconnect()
  4. Event Channel Size - Use bounded channels with reasonable capacity
  5. Read Task - Spawn a separate task for receiving data
  6. Resource Cleanup - Ensure sockets/resources are closed on drop

Testing Transports

Unit test example

See Also