> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP Client Trait

> HTTP client abstraction and ureq implementation for media operations

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

```rust theme={null}
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>;
}
```

<Note>
  `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.
</Note>

<Note>
  `supports_upload_streaming` / `execute_upload` were added in v0.6 to back [`Client::upload_stream`](/api/upload#upload-stream-constant-memory). 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>`.
</Note>

### 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.

```rust theme={null}
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.

```rust theme={null}
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:**

```rust theme={null}
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`.

```rust theme={null}
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:**

```rust theme={null}
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
}
```

#### resource\_report

Best-effort per-session footprint of this client: idle connection-pool buffers plus any in-flight download/media buffering the implementation can see. Defaulted to `None` ("not reported") — a client that can introspect its pool overrides it.

```rust theme={null}
fn resource_report(&self) -> Option<wacore::stats::HttpResourceReport> {
    None
}
```

**Returns:**

* `Some(HttpResourceReport)` with any subset of `pool_connections`, `pool_buffer_bytes`, `inflight_bytes` filled in (each `Option<u64>`)
* `None` (default) if the client doesn't report

Feeds into [`Client::resource_report()`](/api/client#resource_report). `UreqHttpClient` overrides this (below) with an estimate for its default agent's idle pool; media downloads are a real transient-RAM source, so even a coarse estimate is worth reporting.

## Data Structures

### HttpRequest

Represents an HTTP request with headers and optional body.

```rust theme={null}
pub struct HttpRequest {
    pub url: String,
    pub method: String,  // "GET" or "POST"
    pub headers: HashMap<String, String>,
    pub body: Option<Vec<u8>>,
}
```

#### Constructors

```rust theme={null}
// GET request
let request = HttpRequest::get("https://example.com/data");

// POST request
let request = HttpRequest::post("https://example.com/upload");
```

#### Builder Methods

```rust theme={null}
// 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.

```rust theme={null}
pub struct HttpResponse {
    pub status_code: u16,
    pub body: Vec<u8>,
}
```

#### Methods

```rust theme={null}
// Convert body to string
let text = response.body_string()?;
println!("Response text: {}", text);
```

### StreamingHttpResponse

Represents an HTTP response with streaming body reader.

```rust theme={null}
pub struct StreamingHttpResponse {
    pub status_code: u16,
    pub body: Box<dyn std::io::Read + Send>,
}
```

**Usage:**

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
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:**

```rust theme={null}
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](/guides/custom-backends#proxy-and-custom-tls) for a complete guide.

### Usage Examples

#### Basic GET Request

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
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:

```rust theme={null}
#[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:

```rust theme={null}
use ureq::config::Config;
use ureq::tls::TlsConfig;

let agent: ureq::Agent = Config::builder()
    .tls_config(TlsConfig::builder().disable_verification(true).build())
    .build()
    .into();
```

`UreqHttpClient::new()` (the default agent) reports a `resource_report()` estimate for its idle connection pool — up to 3 idle connections, each with a 16 KiB input and 16 KiB output buffer, so `pool_connections: Some(3)`, `pool_buffer_bytes: Some(96 * 1024)`, `inflight_bytes: None`. `UreqHttpClient::with_agent(...)` (a caller-supplied agent) reports `None` — its buffer and pool configuration is opaque, so the client doesn't guess.

The `execute` method clones the agent and wraps the call in `spawn_blocking`:

```rust theme={null}
#[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:

```rust theme={null}
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)

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
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:

```rust theme={null}
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

```rust theme={null}
// 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

```rust theme={null}
#[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

* [Storage Traits](/api/store) - Storage backend abstraction
* [Transport Trait](/api/transport) - Network transport abstraction
* [Client API](/api/client) - Main client interface
* [Media Handling](/guides/media-handling) - Guide to sending and receiving media
