Skip to main content

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;

#[async_trait]
pub trait HttpClient: Send + Sync {
    /// 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>;
}

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

  1. Blocking operations - Always wrap blocking HTTP libraries in tokio::task::spawn_blocking
  2. Streaming for large files - Use execute_streaming for media downloads to avoid buffering
  3. Error handling - Return descriptive errors with context
  4. Timeouts - ureq 3.3 applies per-IP connection timeouts automatically; implement request-level timeouts for reliability
  5. Retries - Consider retry logic for transient failures
  6. Connection pooling - Use a shared ureq::Agent (as UreqHttpClient does) for connection reuse

Media Operations

The HTTP client is primarily used for media operations in whatsapp-rust:

Media Upload Flow

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:
  1. Fetch media connection credentials from WhatsApp servers
  2. Encrypt the media with AES-256-CBC
  3. Upload to the CDN with proper auth headers
  4. Parse the response for direct_path and file hashes

Media Download Flow

// 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