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.
