Overview
The HTTP client abstraction provides a runtime-agnostic interface for making HTTP requests. It’s primarily used for:
- Media uploads to WhatsApp servers
- Media downloads (with streaming support)
- Fetching metadata and authentication tokens
The client supports both buffered and streaming responses for efficient handling of large files.
HttpClient Trait
use async_trait::async_trait;
use wacore::sync_marker::MaybeSendSync;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpClient: MaybeSendSync {
/// Executes a given HTTP request and returns the response
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse>;
/// Whether this client supports synchronous streaming downloads.
/// Defaults to `false`. Override to return `true` if you implement
/// `execute_streaming`.
fn supports_streaming(&self) -> bool { false }
/// Synchronous streaming variant. Returns a reader over the response body
/// instead of buffering it all in memory.
///
/// Must be called from a blocking context (e.g. inside `spawn_blocking`).
/// Returns an error by default if not implemented.
fn execute_streaming(&self, request: HttpRequest) -> Result<StreamingHttpResponse>;
/// Whether this client can stream a request body from a reader (upload).
/// Defaults to `false`. Override alongside `execute_upload` to enable
/// constant-memory uploads via `Client::upload_stream`.
fn supports_upload_streaming(&self) -> bool { false }
/// Synchronous streaming upload: send `body` (exactly `content_length`
/// bytes) as the request body. Implementations MUST set an explicit
/// `Content-Length` header rather than chunked transfer-encoding (the
/// WhatsApp CDN rejects chunked uploads). Any body set on `request` is
/// ignored. Must be called from a blocking context.
fn execute_upload(
&self,
request: HttpRequest,
body: UploadBody, // = Box<dyn std::io::Read + Send>
content_length: u64,
) -> Result<HttpResponse>;
}
MaybeSendSync is Send + Sync on native targets and carries no bounds on wasm32. Custom HttpClient implementations backed by !Send browser fetch handles now compile on the wasm port. On native, Arc<dyn HttpClient> remains Send + Sync as before.
supports_upload_streaming / execute_upload were added in v0.6 to back Client::upload_stream. Both have safe defaults (returning false / an error), so existing custom HttpClient implementations keep compiling — implement them only if you want constant-memory uploads through your client. UreqHttpClient implements both. UploadBody is Box<dyn std::io::Read + Send>.
Methods
supports_streaming
Returns whether this HTTP client supports synchronous streaming downloads via execute_streaming. The default implementation returns false. When this returns false, download_to_writer automatically falls back to a buffered download using execute instead.
fn supports_streaming(&self) -> bool { false }
Returns:
true if execute_streaming is implemented (e.g., UreqHttpClient)
false (default) if streaming is not supported
execute
Executes an HTTP request and buffers the entire response body.
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse>;
Parameters:
request: HttpRequest - The request to execute
Returns:
HttpResponse with buffered body on success
anyhow::Error on failure
Example:
let request = HttpRequest::get("https://api.example.com/data")
.with_header("Authorization", "Bearer token");
let response = client.execute(request).await?;
println!("Status: {}", response.status_code);
println!("Body: {:?}", response.body);
execute_streaming
Executes an HTTP request and returns a streaming reader over the response body.
Important: This is a synchronous method that must be called from within tokio::task::spawn_blocking.
fn execute_streaming(&self, request: HttpRequest) -> Result<StreamingHttpResponse>;
Parameters:
request: HttpRequest - The request to execute
Returns:
StreamingHttpResponse with streaming body reader
anyhow::Error on failure or if streaming is not supported
Example:
let http_client = Arc::new(UreqHttpClient::new());
let request = HttpRequest::get("https://mmg.whatsapp.net/large-file.enc");
// Must be called inside spawn_blocking
let response = tokio::task::spawn_blocking(move || {
http_client.execute_streaming(request)
}).await??;
// Read in chunks
let mut buffer = vec![0u8; 4096];
while let Ok(n) = response.body.read(&mut buffer) {
if n == 0 { break; }
// Process chunk
}
Data Structures
HttpRequest
Represents an HTTP request with headers and optional body.
pub struct HttpRequest {
pub url: String,
pub method: String, // "GET" or "POST"
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
Constructors
// GET request
let request = HttpRequest::get("https://example.com/data");
// POST request
let request = HttpRequest::post("https://example.com/upload");
Builder Methods
// Add header
let request = HttpRequest::get("https://example.com/data")
.with_header("Authorization", "Bearer token")
.with_header("Content-Type", "application/json");
// Add body (for POST)
let body = b"{\"key\": \"value\"}";
let request = HttpRequest::post("https://example.com/upload")
.with_body(body.to_vec());
HttpResponse
Represents an HTTP response with buffered body.
pub struct HttpResponse {
pub status_code: u16,
pub body: Vec<u8>,
}
Methods
// Convert body to string
let text = response.body_string()?;
println!("Response text: {}", text);
StreamingHttpResponse
Represents an HTTP response with streaming body reader.
pub struct StreamingHttpResponse {
pub status_code: u16,
pub body: Box<dyn std::io::Read + Send>,
}
Usage:
use std::io::Read;
let mut buffer = vec![0u8; 8192];
loop {
match response.body.read(&mut buffer) {
Ok(0) => break, // EOF
Ok(n) => {
// Process n bytes from buffer
process_chunk(&buffer[..n]);
}
Err(e) => return Err(e.into()),
}
}
UreqHttpClient
The default HTTP client implementation using the ureq crate (v3.3) for synchronous HTTP requests.
Features
- Blocking I/O - Uses synchronous ureq, wrapped in
tokio::task::spawn_blocking
- Connection pooling - Shares a
ureq::Agent across requests for connection reuse
- Streaming support - Implements efficient streaming downloads
- Simple API - Minimal configuration required
- Thread-safe - Implements
Clone for easy sharing (cloning the Agent is cheap)
- TLS via rustls - Uses rustls for TLS, with optional
danger-skip-tls-verify for testing
Creating a client
use whatsapp_rust_ureq_http_client::UreqHttpClient;
// Basic usage — creates a shared ureq::Agent internally
let client = UreqHttpClient::new();
// Or use default
let client = UreqHttpClient::default();
// With a pre-configured agent (proxy, custom TLS, timeouts, etc.)
let agent = ureq::Agent::new_with_config(
ureq::config::Config::builder()
// your custom settings here
.build()
);
let client = UreqHttpClient::with_agent(agent);
with_agent
pub fn with_agent(agent: ureq::Agent) -> Self
Creates a client with a pre-configured ureq::Agent. This lets you configure proxy support, custom TLS, timeouts, or any other agent-level settings externally.
This is the primary extension point for customizing HTTP behavior — for example, routing media uploads and downloads through a proxy, or using custom CA certificates.
Parameters:
agent - A pre-configured ureq::Agent
Example — proxy support:
use ureq::config::Config;
let agent: ureq::Agent = Config::builder()
.proxy(ureq::Proxy::new("socks5://127.0.0.1:1080")?)
.build()
.into();
let client = UreqHttpClient::with_agent(agent);
See custom backends — proxy and custom TLS for a complete guide.
Usage Examples
Basic GET Request
use whatsapp_rust_ureq_http_client::UreqHttpClient;
use wacore::net::HttpRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = UreqHttpClient::new();
let request = HttpRequest::get("https://api.example.com/data")
.with_header("User-Agent", "whatsapp-rust");
let response = client.execute(request).await?;
if response.status_code == 200 {
println!("Success! Body length: {}", response.body.len());
}
Ok(())
}
POST request with body
let client = UreqHttpClient::new();
let body = serde_json::to_vec(&json!({
"key": "value",
"number": 42
}))?;
let request = HttpRequest::post("https://api.example.com/upload")
.with_header("Content-Type", "application/json")
.with_body(body);
let response = client.execute(request).await?;
Streaming Download
use std::fs::File;
use std::io::Write;
let client = Arc::new(UreqHttpClient::new());
let url = "https://mmg.whatsapp.net/file.enc";
// Must use spawn_blocking for streaming
let file_data = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
let request = HttpRequest::get(url);
let response = client.execute_streaming(request)?;
if response.status_code != 200 {
return Err(anyhow::anyhow!("HTTP {}", response.status_code));
}
let mut buffer = vec![0u8; 65536];
let mut output = Vec::new();
let mut reader = response.body;
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => output.extend_from_slice(&buffer[..n]),
Err(e) => return Err(e.into()),
}
}
Ok(output)
}).await??;
println!("Downloaded {} bytes", file_data.len());
Internal Implementation
The UreqHttpClient wraps a shared ureq::Agent for connection pooling. All requests go through the agent rather than standalone ureq::get()/ureq::post() functions:
#[derive(Debug, Clone)]
pub struct UreqHttpClient {
agent: ureq::Agent,
}
impl UreqHttpClient {
pub fn new() -> Self {
let agent = ureq::Agent::new_with_defaults();
Self { agent }
}
}
When the danger-skip-tls-verify feature is enabled, the agent is built with TLS verification disabled:
use ureq::config::Config;
use ureq::tls::TlsConfig;
let agent: ureq::Agent = Config::builder()
.tls_config(TlsConfig::builder().disable_verification(true).build())
.build()
.into();
The execute method clones the agent and wraps the call in spawn_blocking:
#[async_trait]
impl HttpClient for UreqHttpClient {
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse> {
let agent = self.agent.clone();
// ureq is blocking, so wrap in spawn_blocking
tokio::task::spawn_blocking(move || {
let response = match request.method.as_str() {
"GET" => {
let mut req = agent.get(&request.url);
for (key, value) in &request.headers {
req = req.header(key, value);
}
req.call()?
}
"POST" => {
let mut req = agent.post(&request.url);
for (key, value) in &request.headers {
req = req.header(key, value);
}
if let Some(body) = request.body {
req.send(&body[..])?
} else {
req.send(&[])?
}
}
method => {
return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method));
}
};
let status_code = response.status().as_u16();
let body_bytes = response.into_body().read_to_vec()?;
Ok(HttpResponse {
status_code,
body: body_bytes,
})
})
.await?
}
}
The execute_streaming method is synchronous (no spawn_blocking) because it’s called from within a blocking context. It also uses the shared agent:
fn execute_streaming(&self, request: HttpRequest) -> Result<StreamingHttpResponse> {
let response = match request.method.as_str() {
"GET" => {
let mut req = self.agent.get(&request.url);
for (key, value) in &request.headers {
req = req.header(key, value);
}
req.call()?
}
method => {
return Err(anyhow::anyhow!(
"Streaming only supports GET, got: {}",
method
));
}
};
let status_code = response.status().as_u16();
let reader = response.into_body().into_reader();
Ok(StreamingHttpResponse {
status_code,
body: Box::new(reader),
})
}
Implementing custom HTTP clients
You can implement custom HTTP clients for different runtimes or requirements.
Example: Reqwest client (async)
use async_trait::async_trait;
use wacore::net::{HttpClient, HttpRequest, HttpResponse};
#[derive(Clone)]
pub struct ReqwestHttpClient {
client: reqwest::Client,
}
impl ReqwestHttpClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl HttpClient for ReqwestHttpClient {
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse> {
let mut req = match request.method.as_str() {
"GET" => self.client.get(&request.url),
"POST" => self.client.post(&request.url),
method => return Err(anyhow::anyhow!("Unsupported method: {}", method)),
};
// Add headers
for (key, value) in request.headers {
req = req.header(key, value);
}
// Add body for POST
if let Some(body) = request.body {
req = req.body(body);
}
let response = req.send().await?;
let status_code = response.status().as_u16();
let body = response.bytes().await?.to_vec();
Ok(HttpResponse { status_code, body })
}
// supports_streaming() defaults to false, so download_to_writer
// will use a buffered fallback automatically — no need to implement
// execute_streaming.
}
Example: mock client for testing
use async_trait::async_trait;
use std::collections::HashMap;
pub struct MockHttpClient {
responses: HashMap<String, HttpResponse>,
}
impl MockHttpClient {
pub fn new() -> Self {
Self {
responses: HashMap::new(),
}
}
pub fn with_response(mut self, url: &str, response: HttpResponse) -> Self {
self.responses.insert(url.to_string(), response);
self
}
}
#[async_trait]
impl HttpClient for MockHttpClient {
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse> {
self.responses
.get(&request.url)
.cloned()
.ok_or_else(|| anyhow::anyhow!("No mock response for {}", request.url))
}
}
// Usage in tests:
let client = MockHttpClient::new()
.with_response(
"https://api.example.com/test",
HttpResponse {
status_code: 200,
body: b"test data".to_vec(),
},
);
Usage with Bot builder
use whatsapp_rust::Bot;
use whatsapp_rust_ureq_http_client::UreqHttpClient;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let http_client = UreqHttpClient::new();
let mut bot = Bot::builder()
.with_backend(backend)
.with_transport_factory(transport_factory)
.with_http_client(http_client)
.on_event(|event, client| async move { /* handle events */ })
.build()
.await?;
Ok(())
}
Best Practices
- Blocking operations - Always wrap blocking HTTP libraries in
tokio::task::spawn_blocking
- Streaming for large files - Use
execute_streaming for media downloads to avoid buffering
- Error handling - Return descriptive errors with context
- Timeouts - ureq 3.3 applies per-IP connection timeouts automatically; implement request-level timeouts for reliability
- Retries - Consider retry logic for transient failures
- Connection pooling - Use a shared
ureq::Agent (as UreqHttpClient does) for connection reuse
The HTTP client is primarily used for media operations in whatsapp-rust:
The client manages media connections internally. Use the high-level upload method instead of building requests manually:
use whatsapp_rust::download::MediaType;
// Upload handles encryption, auth, and CDN host selection automatically
let upload_response = client.upload(media_bytes, MediaType::Image, Default::default()).await?;
Under the hood, the client uses the HTTP client to:
- Fetch media connection credentials from WhatsApp servers
- Encrypt the media with AES-256-CBC
- Upload to the CDN with proper auth headers
- Parse the response for
direct_path and file hashes
// 1. Extract media info from message
let media_info = extract_media_info(&message)?;
// 2. Build download URL
let url = format!("https://mmg.whatsapp.net{}", media_info.direct_path);
// 3. Download with streaming (in spawn_blocking)
let file_data = tokio::task::spawn_blocking(move || {
let request = HttpRequest::get(&url);
let response = http_client.execute_streaming(request)?;
// Read and decrypt chunks
decrypt_media_stream(response.body, &media_key, &expected_sha256)
}).await??;
Testing
Unit test example
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_http_get() {
let client = UreqHttpClient::new();
let request = HttpRequest::get("https://httpbin.org/get")
.with_header("User-Agent", "test");
let response = client.execute(request).await.unwrap();
assert_eq!(response.status_code, 200);
}
#[tokio::test]
async fn test_http_post() {
let client = UreqHttpClient::new();
let body = b"test data";
let request = HttpRequest::post("https://httpbin.org/post")
.with_body(body.to_vec());
let response = client.execute(request).await.unwrap();
assert_eq!(response.status_code, 200);
}
}
See Also