Reqwest Rust: Cliente HTTP Async Guia Completo | Rust Brasil

Guia do Reqwest em Rust: GET, POST, async, headers, cookies, TLS, multipart e streaming. O HTTP client mais usado do ecossistema.

Introdução

O Reqwest é o cliente HTTP mais popular e ergonômico do ecossistema Rust. Construído sobre o Hyper, ele oferece uma API de alto nível que simplifica drasticamente a realização de requisições HTTP, mantendo toda a flexibilidade necessária para cenários complexos.

Se você já trabalhou com bibliotecas como requests em Python ou axios em JavaScript, vai se sentir em casa com o Reqwest. Ele abstrai a complexidade do protocolo HTTP e oferece funcionalidades prontas para uso como gerenciamento automático de cookies, suporte a TLS, compressão, redirecionamentos e muito mais.

O Reqwest suporta dois modos de operação: assíncrono (padrão, baseado no Tokio) e blocking (síncrono), permitindo que você escolha a abordagem mais adequada para o seu projeto. A grande maioria dos projetos modernos em Rust utiliza o modo assíncrono, mas o modo blocking é útil para scripts simples e prototipagem rápida.

Por que usar o Reqwest?

  • API ergonômica: métodos encadeáveis e intuitivos
  • Async por padrão: integração nativa com Tokio
  • TLS embutido: suporte a HTTPS sem configuração adicional
  • Gerenciamento de cookies: cookie jar automático
  • Compressão: suporte a gzip, brotli e deflate
  • Proxy: suporte a proxies HTTP e SOCKS5
  • Multipart: upload de arquivos com facilidade
  • Connection pooling: reutilização automática de conexões

Instalação

Adicione o Reqwest ao seu Cargo.toml:

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

As features mais comuns do Reqwest incluem:

[dependencies]
reqwest = { version = "0.12", features = [
    "json",        # Serialização/deserialização JSON
    "cookies",     # Gerenciamento automático de cookies
    "multipart",   # Upload de arquivos multipart
    "stream",      # Streaming de respostas
    "gzip",        # Compressão gzip
    "brotli",      # Compressão brotli
    "rustls-tls",  # TLS via rustls (alternativa ao OpenSSL)
] }

Para o modo blocking (síncrono), adicione a feature correspondente:

[dependencies]
reqwest = { version = "0.12", features = ["json", "blocking"] }

Uso Básico

Requisição GET simples

O uso mais básico do Reqwest é fazer uma requisição GET e obter o corpo da resposta como texto:

use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Requisição GET simples
    let corpo = reqwest::get("https://httpbin.org/get")
        .await?
        .text()
        .await?;

    println!("Resposta: {}", corpo);
    Ok(())
}

Requisição GET com deserialização JSON

Na maioria dos casos, você vai querer deserializar a resposta JSON em uma struct:

use reqwest;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ApiResposta {
    origin: String,
    url: String,
    headers: std::collections::HashMap<String, String>,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let resposta: ApiResposta = reqwest::get("https://httpbin.org/get")
        .await?
        .json()
        .await?;

    println!("Origem: {}", resposta.origin);
    println!("URL: {}", resposta.url);
    Ok(())
}

Usando o Client reutilizável

Para múltiplas requisições, crie um Client que reutiliza conexões (connection pooling):

use reqwest::Client;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Usuario {
    id: u32,
    name: String,
    email: String,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Crie o client uma vez e reutilize
    let client = Client::new();

    // Primeira requisição
    let usuarios: Vec<Usuario> = client
        .get("https://jsonplaceholder.typicode.com/users")
        .send()
        .await?
        .json()
        .await?;

    println!("Total de usuários: {}", usuarios.len());

    // Segunda requisição (reutiliza a conexão)
    let usuario: Usuario = client
        .get("https://jsonplaceholder.typicode.com/users/1")
        .send()
        .await?
        .json()
        .await?;

    println!("Usuário: {} - {}", usuario.name, usuario.email);

    Ok(())
}

Requisição POST com JSON

Enviar dados JSON em uma requisição POST:

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

#[derive(Debug, Serialize)]
struct NovaTarefa {
    title: String,
    body: String,
    #[serde(rename = "userId")]
    user_id: u32,
}

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

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

    let nova_tarefa = NovaTarefa {
        title: "Aprender Reqwest".to_string(),
        body: "Estudar o cliente HTTP do Rust".to_string(),
        user_id: 1,
    };

    let resposta: TarefaCriada = client
        .post("https://jsonplaceholder.typicode.com/posts")
        .json(&nova_tarefa)
        .send()
        .await?
        .json()
        .await?;

    println!("Tarefa criada com ID: {}", resposta.id);
    Ok(())
}

Requisições PUT e DELETE

use reqwest::Client;
use serde::Serialize;

#[derive(Serialize)]
struct AtualizarTarefa {
    title: String,
    body: String,
}

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

    // PUT - Atualizar recurso
    let resposta = client
        .put("https://jsonplaceholder.typicode.com/posts/1")
        .json(&AtualizarTarefa {
            title: "Título atualizado".to_string(),
            body: "Corpo atualizado".to_string(),
        })
        .send()
        .await?;

    println!("PUT status: {}", resposta.status());

    // DELETE - Remover recurso
    let resposta = client
        .delete("https://jsonplaceholder.typicode.com/posts/1")
        .send()
        .await?;

    println!("DELETE status: {}", resposta.status());

    Ok(())
}

Cliente Blocking (Síncrono)

Para scripts simples ou quando você não precisa de async:

use reqwest::blocking::Client;
use serde::Deserialize;

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

fn main() -> Result<(), reqwest::Error> {
    let client = Client::new();

    // GET síncrono
    let posts: Vec<Post> = client
        .get("https://jsonplaceholder.typicode.com/posts")
        .send()?
        .json()?;

    for post in posts.iter().take(5) {
        println!("[{}] {}", post.id, post.title);
    }

    Ok(())
}

Recursos Avançados

Headers personalizados

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

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Headers padrão para todas as requisições do client
    let mut headers_padrao = HeaderMap::new();
    headers_padrao.insert(USER_AGENT, HeaderValue::from_static("minha-app/1.0"));
    headers_padrao.insert(
        AUTHORIZATION,
        HeaderValue::from_static("Bearer meu-token-jwt"),
    );

    let client = Client::builder()
        .default_headers(headers_padrao)
        .build()?;

    // Headers adicionais por requisição
    let resposta = client
        .get("https://httpbin.org/headers")
        .header(CONTENT_TYPE, "application/json")
        .header("X-Custom-Header", "valor-personalizado")
        .send()
        .await?;

    println!("Status: {}", resposta.status());
    println!("Corpo: {}", resposta.text().await?);

    Ok(())
}

Cookies

use reqwest::{cookie::Jar, Client, Url};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Cookie jar para gerenciar cookies manualmente
    let jar = Arc::new(Jar::default());

    // Adicionar cookie manualmente
    let url = "https://httpbin.org".parse::<Url>().unwrap();
    jar.add_cookie_str("sessao=abc123; Domain=httpbin.org", &url);

    let client = Client::builder()
        .cookie_store(true)
        .cookie_provider(jar.clone())
        .build()?;

    let resposta = client
        .get("https://httpbin.org/cookies")
        .send()
        .await?
        .text()
        .await?;

    println!("Cookies: {}", resposta);
    Ok(())
}

Timeouts e configuração do Client

use reqwest::Client;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = Client::builder()
        // Timeout total da requisição
        .timeout(Duration::from_secs(30))
        // Timeout para estabelecer conexão
        .connect_timeout(Duration::from_secs(5))
        // Número máximo de redirecionamentos
        .redirect(reqwest::redirect::Policy::limited(5))
        // Habilitar compressão gzip
        .gzip(true)
        // Configurar proxy
        // .proxy(reqwest::Proxy::all("http://proxy:8080")?)
        // Usar rustls ao invés de OpenSSL nativo
        // .use_rustls_tls()
        .build()?;

    let resposta = client
        .get("https://httpbin.org/delay/2")
        .send()
        .await?;

    println!("Status: {}", resposta.status());
    Ok(())
}

Multipart uploads (envio de arquivos)

use reqwest::{multipart, Client};
use tokio::fs;

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

    // Ler arquivo do disco
    let conteudo_arquivo = fs::read("foto.jpg").await?;

    // Criar formulário multipart
    let form = multipart::Form::new()
        .text("descricao", "Minha foto de perfil")
        .text("usuario", "joao_silva")
        .part(
            "arquivo",
            multipart::Part::bytes(conteudo_arquivo)
                .file_name("foto.jpg")
                .mime_str("image/jpeg")?,
        );

    let resposta = client
        .post("https://httpbin.org/post")
        .multipart(form)
        .send()
        .await?;

    println!("Status: {}", resposta.status());
    println!("Resposta: {}", resposta.text().await?);

    Ok(())
}

Streaming de respostas (download de arquivos grandes)

use reqwest::Client;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

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

    let mut resposta = client
        .get("https://example.com/arquivo-grande.zip")
        .send()
        .await?;

    // Verificar status antes de processar
    if !resposta.status().is_success() {
        eprintln!("Erro: status {}", resposta.status());
        return Ok(());
    }

    // Tamanho total (se disponível)
    let tamanho_total = resposta.content_length();
    println!(
        "Tamanho total: {} bytes",
        tamanho_total
            .map(|t| t.to_string())
            .unwrap_or_else(|| "desconhecido".to_string())
    );

    let mut arquivo = File::create("download.zip").await?;
    let mut bytes_baixados: u64 = 0;

    // Fazer download em chunks
    while let Some(chunk) = resposta.chunk().await? {
        arquivo.write_all(&chunk).await?;
        bytes_baixados += chunk.len() as u64;

        if let Some(total) = tamanho_total {
            let progresso = (bytes_baixados as f64 / total as f64) * 100.0;
            print!("\rProgresso: {:.1}%", progresso);
        }
    }

    println!("\nDownload concluído!");
    Ok(())
}

Query parameters

use reqwest::Client;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ResultadoBusca {
    args: std::collections::HashMap<String, String>,
}

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

    // Parâmetros via método query()
    let resultado: ResultadoBusca = client
        .get("https://httpbin.org/get")
        .query(&[
            ("busca", "rust lang"),
            ("pagina", "1"),
            ("limite", "10"),
        ])
        .send()
        .await?
        .json()
        .await?;

    println!("Parâmetros enviados: {:?}", resultado.args);

    // Parâmetros via struct com Serialize
    #[derive(serde::Serialize)]
    struct Parametros {
        busca: String,
        pagina: u32,
        limite: u32,
    }

    let params = Parametros {
        busca: "rust reqwest".to_string(),
        pagina: 1,
        limite: 20,
    };

    let resultado: ResultadoBusca = client
        .get("https://httpbin.org/get")
        .query(&params)
        .send()
        .await?
        .json()
        .await?;

    println!("Parâmetros: {:?}", resultado.args);
    Ok(())
}

Tratamento robusto de erros

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

#[derive(Debug, Deserialize)]
struct ApiErro {
    mensagem: String,
    codigo: u32,
}

#[derive(Debug)]
enum AppErro {
    Rede(reqwest::Error),
    Api(StatusCode, String),
    Deserializacao(String),
}

impl From<reqwest::Error> for AppErro {
    fn from(err: reqwest::Error) -> Self {
        AppErro::Rede(err)
    }
}

async fn buscar_dados(client: &Client, url: &str) -> Result<String, AppErro> {
    let resposta = client.get(url).send().await?;

    match resposta.status() {
        StatusCode::OK => {
            let texto = resposta.text().await?;
            Ok(texto)
        }
        StatusCode::NOT_FOUND => {
            Err(AppErro::Api(StatusCode::NOT_FOUND, "Recurso não encontrado".to_string()))
        }
        StatusCode::UNAUTHORIZED => {
            Err(AppErro::Api(StatusCode::UNAUTHORIZED, "Não autorizado".to_string()))
        }
        StatusCode::TOO_MANY_REQUESTS => {
            Err(AppErro::Api(
                StatusCode::TOO_MANY_REQUESTS,
                "Rate limit excedido, tente novamente mais tarde".to_string(),
            ))
        }
        status => {
            let corpo = resposta.text().await.unwrap_or_default();
            Err(AppErro::Api(status, corpo))
        }
    }
}

#[tokio::main]
async fn main() {
    let client = Client::new();

    match buscar_dados(&client, "https://httpbin.org/status/404").await {
        Ok(dados) => println!("Sucesso: {}", dados),
        Err(AppErro::Rede(e)) => eprintln!("Erro de rede: {}", e),
        Err(AppErro::Api(status, msg)) => eprintln!("Erro da API [{}]: {}", status, msg),
        Err(AppErro::Deserializacao(msg)) => eprintln!("Erro de parse: {}", msg),
    }
}

Retry com backoff exponencial

use reqwest::Client;
use std::time::Duration;
use tokio::time::sleep;

async fn requisicao_com_retry(
    client: &Client,
    url: &str,
    tentativas_max: u32,
) -> Result<String, reqwest::Error> {
    let mut tentativa = 0;

    loop {
        tentativa += 1;
        println!("Tentativa {} de {}...", tentativa, tentativas_max);

        match client.get(url).send().await {
            Ok(resposta) if resposta.status().is_success() => {
                return resposta.text().await;
            }
            Ok(resposta) if resposta.status().is_server_error() && tentativa < tentativas_max => {
                let espera = Duration::from_millis(100 * 2u64.pow(tentativa - 1));
                eprintln!(
                    "Erro do servidor ({}), tentando novamente em {:?}...",
                    resposta.status(),
                    espera
                );
                sleep(espera).await;
            }
            Ok(resposta) => {
                return resposta.text().await;
            }
            Err(e) if tentativa < tentativas_max => {
                let espera = Duration::from_millis(100 * 2u64.pow(tentativa - 1));
                eprintln!("Erro de rede: {}, tentando novamente em {:?}...", e, espera);
                sleep(espera).await;
            }
            Err(e) => return Err(e),
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;

    let resultado = requisicao_com_retry(&client, "https://httpbin.org/get", 3).await?;
    println!("Resultado: {}", resultado);
    Ok(())
}

Boas Práticas

1. Reutilize o Client

Sempre crie um único Client e reutilize-o para todas as requisições. O Client gerencia um pool de conexões internamente:

use reqwest::Client;
use std::sync::Arc;

// Em uma aplicação web, compartilhe o client via estado
struct AppState {
    http_client: Client,
}

// Ou use Arc para compartilhar entre threads
async fn exemplo() {
    let client = Arc::new(Client::new());

    let client_clone = client.clone();
    let handle = tokio::spawn(async move {
        client_clone
            .get("https://httpbin.org/get")
            .send()
            .await
    });

    let _ = handle.await;
}

2. Sempre verifique o status da resposta

use reqwest::Client;

async fn buscar_seguro(client: &Client, url: &str) -> Result<String, String> {
    let resposta = client
        .get(url)
        .send()
        .await
        .map_err(|e| format!("Erro de rede: {}", e))?;

    // error_for_status() converte 4xx/5xx em erro
    let resposta = resposta
        .error_for_status()
        .map_err(|e| format!("Erro HTTP: {}", e))?;

    resposta
        .text()
        .await
        .map_err(|e| format!("Erro ao ler corpo: {}", e))
}

3. Configure timeouts apropriados

use reqwest::Client;
use std::time::Duration;

fn criar_client_producao() -> Client {
    Client::builder()
        .timeout(Duration::from_secs(30))
        .connect_timeout(Duration::from_secs(5))
        .pool_idle_timeout(Duration::from_secs(90))
        .pool_max_idle_per_host(10)
        .build()
        .expect("Falha ao criar HTTP client")
}

4. Use tipos fortes para respostas da API

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct PaginaDeResultados<T> {
    dados: Vec<T>,
    total: u64,
    pagina: u32,
    por_pagina: u32,
}

#[derive(Debug, Deserialize)]
struct Produto {
    id: u64,
    nome: String,
    preco: f64,
    #[serde(default)]
    disponivel: bool,
}

// Uso type-safe
async fn listar_produtos(
    client: &reqwest::Client,
) -> Result<PaginaDeResultados<Produto>, reqwest::Error> {
    client
        .get("https://api.exemplo.com/produtos")
        .query(&[("pagina", "1"), ("limite", "20")])
        .send()
        .await?
        .json()
        .await
}

5. Centralize a configuração do client

use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION};
use reqwest::Client;
use std::time::Duration;

struct ApiClient {
    client: Client,
    base_url: String,
}

impl ApiClient {
    fn new(base_url: &str, token: &str) -> Result<Self, reqwest::Error> {
        let mut headers = HeaderMap::new();
        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_str(&format!("Bearer {}", token))
                .expect("Token inválido"),
        );

        let client = Client::builder()
            .default_headers(headers)
            .timeout(Duration::from_secs(30))
            .build()?;

        Ok(Self {
            client,
            base_url: base_url.to_string(),
        })
    }

    async fn get<T: serde::de::DeserializeOwned>(
        &self,
        caminho: &str,
    ) -> Result<T, reqwest::Error> {
        let url = format!("{}{}", self.base_url, caminho);
        self.client.get(&url).send().await?.json().await
    }

    async fn post<B: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        caminho: &str,
        corpo: &B,
    ) -> Result<T, reqwest::Error> {
        let url = format!("{}{}", self.base_url, caminho);
        self.client.post(&url).json(corpo).send().await?.json().await
    }
}

Exemplos Práticos

Consumindo uma API REST completa

Este exemplo mostra como criar um cliente completo para uma API REST de gerenciamento de tarefas:

use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use std::time::Duration;

// === Modelos ===

#[derive(Debug, Serialize)]
struct CriarTarefa {
    titulo: String,
    descricao: String,
    prioridade: Prioridade,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Prioridade {
    Baixa,
    Media,
    Alta,
}

#[derive(Debug, Deserialize)]
struct Tarefa {
    id: u64,
    titulo: String,
    descricao: String,
    prioridade: Prioridade,
    concluida: bool,
}

#[derive(Debug, Deserialize)]
struct ListaResposta {
    tarefas: Vec<Tarefa>,
    total: u64,
}

// === Cliente da API ===

struct TarefasApi {
    client: Client,
    base_url: String,
}

impl TarefasApi {
    fn new(base_url: &str, token: &str) -> Result<Self, reqwest::Error> {
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            reqwest::header::AUTHORIZATION,
            reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))
                .expect("Token inválido"),
        );

        let client = Client::builder()
            .default_headers(headers)
            .timeout(Duration::from_secs(30))
            .connect_timeout(Duration::from_secs(5))
            .build()?;

        Ok(Self {
            client,
            base_url: base_url.to_string(),
        })
    }

    async fn listar(&self, pagina: u32) -> Result<ListaResposta, reqwest::Error> {
        self.client
            .get(format!("{}/tarefas", self.base_url))
            .query(&[("pagina", pagina.to_string()), ("limite", "20".to_string())])
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    async fn obter(&self, id: u64) -> Result<Option<Tarefa>, reqwest::Error> {
        let resposta = self.client
            .get(format!("{}/tarefas/{}", self.base_url, id))
            .send()
            .await?;

        match resposta.status() {
            StatusCode::OK => Ok(Some(resposta.json().await?)),
            StatusCode::NOT_FOUND => Ok(None),
            _ => {
                resposta.error_for_status()?;
                unreachable!()
            }
        }
    }

    async fn criar(&self, tarefa: &CriarTarefa) -> Result<Tarefa, reqwest::Error> {
        self.client
            .post(format!("{}/tarefas", self.base_url))
            .json(tarefa)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    async fn concluir(&self, id: u64) -> Result<Tarefa, reqwest::Error> {
        self.client
            .patch(format!("{}/tarefas/{}", self.base_url, id))
            .json(&serde_json::json!({"concluida": true}))
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    async fn excluir(&self, id: u64) -> Result<bool, reqwest::Error> {
        let resposta = self.client
            .delete(format!("{}/tarefas/{}", self.base_url, id))
            .send()
            .await?;

        Ok(resposta.status() == StatusCode::NO_CONTENT)
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api = TarefasApi::new("https://api.exemplo.com", "meu-token")?;

    // Criar uma nova tarefa
    let nova = api.criar(&CriarTarefa {
        titulo: "Aprender Reqwest".to_string(),
        descricao: "Estudar o cliente HTTP do Rust".to_string(),
        prioridade: Prioridade::Alta,
    }).await?;
    println!("Tarefa criada: {:?}", nova);

    // Listar tarefas
    let lista = api.listar(1).await?;
    println!("Total de tarefas: {}", lista.total);
    for tarefa in &lista.tarefas {
        println!(
            "  [{}] {} (concluída: {})",
            tarefa.id, tarefa.titulo, tarefa.concluida
        );
    }

    // Marcar como concluída
    let concluida = api.concluir(nova.id).await?;
    println!("Tarefa concluída: {:?}", concluida);

    // Excluir
    let excluida = api.excluir(nova.id).await?;
    println!("Tarefa excluída: {}", excluida);

    Ok(())
}

Requisições paralelas com Tokio

use reqwest::Client;
use serde::Deserialize;

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

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

    // Fazer múltiplas requisições em paralelo
    let urls = vec![
        "https://jsonplaceholder.typicode.com/posts/1",
        "https://jsonplaceholder.typicode.com/posts/2",
        "https://jsonplaceholder.typicode.com/posts/3",
        "https://jsonplaceholder.typicode.com/posts/4",
        "https://jsonplaceholder.typicode.com/posts/5",
    ];

    let futures: Vec<_> = urls
        .into_iter()
        .map(|url| {
            let client = client.clone();
            tokio::spawn(async move {
                client.get(url).send().await?.json::<Post>().await
            })
        })
        .collect();

    for future in futures {
        match future.await? {
            Ok(post) => println!("[{}] {}", post.id, post.title),
            Err(e) => eprintln!("Erro: {}", e),
        }
    }

    Ok(())
}

Comparação com Alternativas

CaracterísticaReqwestHyperureqsurf
NívelAlto nívelBaixo nívelAlto nívelAlto nível
AsyncSim (padrão)SimNão (blocking)Sim
BlockingOpcionalNãoSim (padrão)Não
TLSEmbutidoManualEmbutidoEmbutido
CookiesAutomáticoManualManualManual
JSONVia featureManualVia featureVia feature
MultipartVia featureManualManualVia feature
Connection poolAutomáticoManualAutomáticoAutomático
DependênciasMédiaMínimaMínimaMédia
  • Reqwest vs Hyper: Use Reqwest para consumir APIs e Hyper quando precisar de controle total sobre o protocolo HTTP ou estiver construindo frameworks.
  • Reqwest vs ureq: Use ureq se não precisa de async e quer o mínimo de dependências. Reqwest é mais completo e versátil.
  • Reqwest vs surf: Surf é baseado em async-std ao invés de Tokio. Se seu projeto já usa Tokio, escolha Reqwest.

Conclusão

O Reqwest é a escolha padrão para fazer requisições HTTP em Rust e por boas razões. Sua API ergonômica, suporte completo a async, gerenciamento automático de conexões e funcionalidades prontas como TLS, cookies e compressão tornam-no indispensável para qualquer projeto que precisa se comunicar via HTTP.

Para a maioria dos casos de uso – consumir APIs REST, fazer downloads, enviar webhooks, integrar com serviços externos – o Reqwest resolve com poucas linhas de código e alta confiabilidade.

Próximos passos

  • Explore o Hyper para entender a camada HTTP de baixo nível
  • Combine Reqwest com Tower para adicionar middleware como retry e rate limiting
  • Use Tokio para orquestrar múltiplas requisições em paralelo
  • Aprenda sobre Tracing para observar suas requisições HTTP em produção