reqwest es el cliente HTTP asíncrono más usado en el ecosistema Rust. Se integra con Tokio, soporta HTTP/1.1 y HTTP/2, y su API es lo suficientemente ergonómica como para que una petición sencilla sean tres líneas de código. Aquí cubrimos desde la llamada más básica hasta autenticación Bearer, JSON con serde, descarga de ficheros y el error más común que encontrarás al empezar.
Instalación
// Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
GET simple
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let texto = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("{texto}");
Ok(())
}
Client con pool de conexiones y timeout
use reqwest::Client;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
// Crea el Client una sola vez y clónalo; comparte el pool de conexiones
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let respuesta = client
.get("https://httpbin.org/delay/1")
.send()
.await?;
println!("Status: {}", respuesta.status());
println!("Headers: {:#?}", respuesta.headers());
Ok(())
}
Deserializar JSON con serde
use reqwest::Client;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Post {
id: u32,
title: String,
body: String,
#[serde(rename = "userId")]
user_id: u32,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new();
let post: Post = client
.get("https://jsonplaceholder.typicode.com/posts/1")
.send()
.await?
.json::<Post>() // necesita el feature "json" activado
.await?;
println!("{:#?}", post);
Ok(())
}
POST con JSON y autenticación Bearer
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct NuevoPost {
title: String,
body: String,
user_id: u32,
}
#[derive(Deserialize, Debug)]
struct RespuestaPost {
id: u32,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::new();
let token = "mi_token_secreto";
let payload = NuevoPost {
title: "Mi artículo".into(),
body: "Contenido del artículo".into(),
user_id: 1,
};
let respuesta: RespuestaPost = client
.post("https://jsonplaceholder.typicode.com/posts")
.bearer_auth(token)
.json(&payload)
.send()
.await?
.error_for_status()? // convierte 4xx/5xx en Err
.json()
.await?;
println!("Creado con id: {}", respuesta.id);
Ok(())
}
Descarga de ficheros
use reqwest::Client;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::new();
let mut respuesta = client
.get("https://httpbin.org/bytes/1024")
.send()
.await?;
let mut fichero = File::create("descarga.bin").await?;
// Procesar en chunks para no cargar todo en memoria
while let Some(chunk) = respuesta.chunk().await? {
fichero.write_all(&chunk).await?;
}
println!("Descarga completada");
Ok(())
}
El error más común: feature json sin activar
// Si ves este error al compilar:
// error[E0277]: the trait bound `Post: DeserializeOwned` is not satisfied
//
// o este otro en tiempo de compilación:
// method not found in `reqwest::Response`: json
//
// La causa: el feature "json" no está activado en Cargo.toml
//
// INCORRECTO:
// reqwest = "0.11"
//
// CORRECTO:
// reqwest = { version = "0.11", features = ["json"] }
Resumen
- Crea un
Clientuna vez y clónalo: comparte pool de conexiones y configuración. .json::<T>()deserializa directamente siTimplementaDeserializey el featurejsonestá activo..json(&payload)serializa el cuerpo y añadeContent-Type: application/json..bearer_auth(token)añade el headerAuthorization: Bearer ...sin formateo manual..error_for_status()convierte respuestas 4xx/5xx enErrautomáticamente.- Para ficheros grandes usa
.chunk()en un bucle: evita cargar todo en memoria.
