reqwest en Rust: cliente HTTP asíncrono, GET/POST, headers, JSON y manejo de errores

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 Client una vez y clónalo: comparte pool de conexiones y configuración.
  • .json::<T>() deserializa directamente si T implementa Deserialize y el feature json está activo.
  • .json(&payload) serializa el cuerpo y añade Content-Type: application/json.
  • .bearer_auth(token) añade el header Authorization: Bearer ... sin formateo manual.
  • .error_for_status() convierte respuestas 4xx/5xx en Err automáticamente.
  • Para ficheros grandes usa .chunk() en un bucle: evita cargar todo en memoria.

COMPARTE ESTE ARTÍCULO

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