reqwest en Rust: cliente HTTP async, headers, middleware con Tower y streaming de respuestas

reqwest es el cliente HTTP async más usado en Rust. Construido sobre Tokio e Hyper, ofrece una API de alto nivel para peticiones GET, POST y el resto de verbos HTTP, manejo de headers, autenticación, streaming de respuestas y, con reqwest-middleware, soporte de middleware para reintentos automáticos y logging.

Peticiones GET y POST básicas

El punto de entrada es reqwest::Client, que gestiona el pool de conexiones y puede reutilizarse durante toda la vida de la aplicación. Para peticiones únicas también existe reqwest::get(), pero crear el Client en cada llamada es ineficiente.

// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde = { version = "1", features = ["derive"] }

use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Debug)]
struct Post {
    id: u32,
    title: String,
    body: String,
}

#[derive(Serialize)]
struct NuevoPost {
    title: String,
    body: String,
    user_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = Client::new();

    // GET con respuesta JSON
    let post: Post = client
        .get("https://jsonplaceholder.typicode.com/posts/1")
        .send()
        .await?
        .error_for_status()? // error si HTTP 4xx/5xx
        .json()
        .await?;
    println!("Título: {}", post.title);

    // POST con body JSON
    let nuevo = NuevoPost { title: "Rust es genial".into(), body: "...".into(), user_id: 1 };
    let creado: Post = client
        .post("https://jsonplaceholder.typicode.com/posts")
        .json(&nuevo)
        .send()
        .await?
        .json()
        .await?;
    println!("Creado con id: {}", creado.id);

    Ok(())
}

Headers con HeaderMap y autenticación Bearer

Los headers pueden añadirse en la construcción del Client (para headers por defecto en todas las peticiones) o en cada petición individual. La autenticación Bearer es el patrón más habitual con APIs REST modernas.

use reqwest::{Client, header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}};

fn crear_cliente(token: &str) -> reqwest::Result<Client> {
    let mut headers = HeaderMap::new();
    headers.insert(
        AUTHORIZATION,
        HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
    );
    headers.insert(USER_AGENT, HeaderValue::from_static("mi-app/1.0"));
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

    Client::builder()
        .default_headers(headers)
        .timeout(std::time::Duration::from_secs(10))
        .build()
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = crear_cliente("mi-token-secreto")?;

    // Todos los headers por defecto se incluyen automáticamente
    let resp = client
        .get("https://api.ejemplo.com/datos")
        .header("X-Request-ID", "abc-123") // header adicional solo en esta petición
        .send()
        .await?;

    println!("Status: {}", resp.status());
    println!("Content-Type: {:?}", resp.headers().get(CONTENT_TYPE));
    Ok(())
}

Streaming de respuestas con bytes_stream

Para respuestas grandes (descargas de archivos, exports CSV, streams de eventos) es mejor no cargar todo el cuerpo en memoria. bytes_stream() devuelve un Stream de chunks de bytes que pueden procesarse o escribirse a disco de forma incremental.

use reqwest::Client;
use tokio::io::AsyncWriteExt;
use futures_util::StreamExt;

async fn descargar_archivo(url: &str, destino: &str) -> anyhow::Result<u64> {
    let client = Client::new();
    let mut resp = client.get(url).send().await?.error_for_status()?;

    let total = resp.content_length().unwrap_or(0);
    let mut archivo = tokio::fs::File::create(destino).await?;
    let mut descargados: u64 = 0;

    while let Some(chunk) = resp.chunk().await? {
        archivo.write_all(&chunk).await?;
        descargados += chunk.len() as u64;
        if total > 0 {
            print!("r{:.1}%", descargados as f64 / total as f64 * 100.0);
        }
    }
    archivo.flush().await?;
    println!("nDescargados {descargados} bytes ? {destino}");
    Ok(descargados)
}

Middleware con reqwest-middleware y reintentos automáticos

reqwest-middleware envuelve el Client para añadir comportamientos transversales: logging, métricas, reintentos con backoff exponencial y circuit breakers. reqwest-retry proporciona la política de reintentos más común.

// [dependencies]
// reqwest-middleware = "0.3"
// reqwest-retry = "0.6"

use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};

fn crear_cliente_con_reintentos() -> ClientWithMiddleware {
    let politica = ExponentialBackoff::builder()
        .retry_bounds(
            std::time::Duration::from_millis(100),
            std::time::Duration::from_secs(5),
        )
        .build_with_max_retries(3);

    ClientBuilder::new(reqwest::Client::new())
        .with(RetryTransientMiddleware::new_with_policy(politica))
        .build()
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = crear_cliente_con_reintentos();

    // Reintentos automáticos en errores transitorios (5xx, timeout, red)
    let resp = client
        .get("https://api.ejemplo.com/datos")
        .send()
        .await?;

    println!("Status final: {}", resp.status());
    // Si falla con 503, reintenta hasta 3 veces con backoff exponencial
    Ok(())
}

En proyectos de producción, la combinación de Client con headers por defecto, timeout global, middleware de reintentos y streaming para archivos grandes cubre la gran mayoría de casos de uso de cliente HTTP en Rust. Crea el Client una vez al inicio y compártelo con Arc o como estado de Axum; nunca lo recrees en cada petición.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP