Reqwest: Guia Completo do HTTP Client Rust | Rust Brasil

Guia completo do Reqwest em Rust: GET, POST, JSON, headers, cookies, streaming, proxy e TLS. O HTTP client mais popular do ecossistema.

Introdução

Reqwest é o cliente HTTP mais popular do ecossistema Rust. Construído sobre o Hyper e o Tokio, ele oferece uma API ergonômica para fazer requisições HTTP/HTTPS com suporte a async/await, JSON, formulários, cookies, proxy, streaming e muito mais.

Se você precisa consumir APIs REST, fazer web scraping, baixar arquivos ou interagir com qualquer serviço HTTP, o Reqwest é a escolha padrão. Ele é usado por praticamente toda aplicação Rust que faz requisições de rede.

Neste guia, vamos cobrir desde requisições simples até padrões avançados de produção como retry, timeout, rate limiting e streaming.

Dependências no Cargo.toml

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

Features opcionais do Reqwest:

# TLS nativo do sistema (ao invés do rustls padrão)
reqwest = { version = "0.12", features = ["json", "native-tls"] }

# Para requisições bloqueantes (sem async)
reqwest = { version = "0.12", features = ["json", "blocking"] }

Requisições Básicas

GET Simples

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

    println!("Resposta: {body}");
    Ok(())
}

GET com Deserialização JSON

use serde::Deserialize;

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

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

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

POST com JSON

use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct CriarUsuario {
    nome: String,
    email: String,
    idade: u32,
}

#[derive(Debug, Deserialize)]
struct UsuarioCriado {
    id: u64,
    nome: String,
    email: String,
}

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

    let novo_usuario = CriarUsuario {
        nome: "Maria Silva".to_string(),
        email: "maria@exemplo.com".to_string(),
        idade: 28,
    };

    let resposta: UsuarioCriado = client
        .post("https://httpbin.org/post")
        .json(&novo_usuario)
        .send()
        .await?
        .json()
        .await?;

    println!("Usuário criado: {resposta:?}");
    Ok(())
}

O Client Reutilizável

Sempre use um Client reutilizável ao invés de reqwest::get() — ele mantém um pool de conexões:

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

fn criar_client() -> Client {
    Client::builder()
        .timeout(Duration::from_secs(30))
        .connect_timeout(Duration::from_secs(10))
        .pool_max_idle_per_host(10)
        .user_agent("meu-app/1.0")
        .build()
        .expect("Falha ao criar HTTP client")
}

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

    // Reutilizar o mesmo client para múltiplas requisições
    let r1 = client.get("https://httpbin.org/get").send().await?;
    let r2 = client.get("https://httpbin.org/ip").send().await?;

    println!("Status 1: {}", r1.status());
    println!("Status 2: {}", r2.status());
    Ok(())
}

Headers Customizados

use reqwest::{Client, header};

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Headers padrão para todas as requisições
    let mut headers = header::HeaderMap::new();
    headers.insert(
        header::ACCEPT,
        header::HeaderValue::from_static("application/json"),
    );
    headers.insert(
        header::ACCEPT_LANGUAGE,
        header::HeaderValue::from_static("pt-BR"),
    );

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

    // Headers adicionais por requisição
    let resposta = client
        .get("https://httpbin.org/headers")
        .header("X-Custom-Header", "valor-customizado")
        .bearer_auth("meu-token-jwt-aqui")
        .send()
        .await?;

    println!("{}", resposta.text().await?);
    Ok(())
}

Autenticação

use reqwest::Client;

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

    // Basic Auth
    let r = client
        .get("https://httpbin.org/basic-auth/user/pass")
        .basic_auth("user", Some("pass"))
        .send()
        .await?;
    println!("Basic Auth: {}", r.status());

    // Bearer Token
    let r = client
        .get("https://api.exemplo.com/dados")
        .bearer_auth("eyJhbGciOiJIUzI1NiIs...")
        .send()
        .await?;
    println!("Bearer: {}", r.status());

    Ok(())
}

Query Parameters

use serde::Serialize;

#[derive(Serialize)]
struct FiltrosBusca {
    q: String,
    pagina: u32,
    por_pagina: u32,
    ordenar: String,
}

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

    // Via struct
    let filtros = FiltrosBusca {
        q: "rust programming".to_string(),
        pagina: 1,
        por_pagina: 20,
        ordenar: "relevancia".to_string(),
    };

    let r = client
        .get("https://httpbin.org/get")
        .query(&filtros)
        .send()
        .await?;

    println!("URL: {}", r.url());
    // https://httpbin.org/get?q=rust+programming&pagina=1&por_pagina=20&ordenar=relevancia

    // Via tuplas
    let r = client
        .get("https://httpbin.org/get")
        .query(&[("q", "rust"), ("page", "1")])
        .send()
        .await?;

    println!("{}", r.text().await?);
    Ok(())
}

Formulários e Multipart

Form URL-encoded

use serde::Serialize;

#[derive(Serialize)]
struct LoginForm {
    username: String,
    password: String,
}

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

    let form = LoginForm {
        username: "usuario".to_string(),
        password: "senha123".to_string(),
    };

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

    println!("{}", r.text().await?);
    Ok(())
}

Multipart (Upload de Arquivos)

use reqwest::multipart;
use tokio::fs;

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

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

    let form = multipart::Form::new()
        .text("descricao", "Minha foto")
        .part("arquivo", multipart::Part::bytes(conteudo)
            .file_name("foto.jpg")
            .mime_str("image/jpeg")?
        );

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

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

Cookies

use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Client com cookie jar
    let client = Client::builder()
        .cookie_store(true)
        .build()?;

    // Primeira requisição define cookies
    client
        .get("https://httpbin.org/cookies/set/sessao/abc123")
        .send()
        .await?;

    // Segunda requisição envia os cookies automaticamente
    let r = client
        .get("https://httpbin.org/cookies")
        .send()
        .await?;

    println!("Cookies: {}", r.text().await?);
    Ok(())
}

Tratamento de Erros e Status

use reqwest::StatusCode;
use serde::Deserialize;

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

#[derive(Debug, Deserialize)]
struct Dados {
    resultado: String,
}

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

    match resposta.status() {
        StatusCode::OK => {
            let dados = resposta.json::<Dados>().await?;
            Ok(dados)
        }
        StatusCode::NOT_FOUND => {
            anyhow::bail!("Recurso não encontrado: {url}")
        }
        StatusCode::UNAUTHORIZED => {
            anyhow::bail!("Não autorizado — verifique suas credenciais")
        }
        StatusCode::TOO_MANY_REQUESTS => {
            let retry_after = resposta
                .headers()
                .get("retry-after")
                .and_then(|v| v.to_str().ok())
                .unwrap_or("desconhecido");
            anyhow::bail!("Rate limit excedido. Retry-After: {retry_after}")
        }
        status => {
            let body = resposta.text().await.unwrap_or_default();
            anyhow::bail!("Erro HTTP {status}: {body}")
        }
    }
}

// error_for_status() converte 4xx/5xx em erro automaticamente
async fn buscar_simples(client: &reqwest::Client, url: &str) -> reqwest::Result<String> {
    client
        .get(url)
        .send()
        .await?
        .error_for_status()?  // Retorna Err para status 4xx/5xx
        .text()
        .await
}

Timeout e Retry

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

struct HttpClient {
    client: Client,
    max_retries: u32,
    base_delay: Duration,
}

impl HttpClient {
    fn new() -> Self {
        let client = Client::builder()
            .timeout(Duration::from_secs(30))
            .connect_timeout(Duration::from_secs(10))
            .build()
            .unwrap();

        Self {
            client,
            max_retries: 3,
            base_delay: Duration::from_millis(500),
        }
    }

    async fn get_com_retry(&self, url: &str) -> Result<String, reqwest::Error> {
        let mut ultimo_erro = None;

        for tentativa in 0..=self.max_retries {
            if tentativa > 0 {
                let delay = self.base_delay * 2u32.pow(tentativa - 1);
                eprintln!("Tentativa {} de {}. Aguardando {:?}...",
                    tentativa + 1, self.max_retries + 1, delay);
                sleep(delay).await;
            }

            match self.client.get(url).send().await {
                Ok(resposta) => {
                    if resposta.status().is_server_error() && tentativa < self.max_retries {
                        eprintln!("Erro do servidor: {}", resposta.status());
                        continue;
                    }
                    return resposta.text().await;
                }
                Err(e) => {
                    eprintln!("Erro na requisição: {e}");
                    ultimo_erro = Some(e);
                }
            }
        }

        Err(ultimo_erro.unwrap())
    }
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = HttpClient::new();
    let body = client.get_com_retry("https://httpbin.org/get").await?;
    println!("{body}");
    Ok(())
}

Streaming: Download de Arquivos Grandes

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

async fn download_arquivo(url: &str, destino: &str) -> Result<u64, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let resposta = client.get(url).send().await?.error_for_status()?;

    let tamanho_total = resposta.content_length().unwrap_or(0);
    println!("Tamanho total: {} bytes", tamanho_total);

    let mut arquivo = File::create(destino).await?;
    let mut stream = resposta.bytes_stream();
    let mut baixado: u64 = 0;

    use futures_util::StreamExt;
    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        arquivo.write_all(&chunk).await?;
        baixado += chunk.len() as u64;

        if tamanho_total > 0 {
            let progresso = (baixado as f64 / tamanho_total as f64) * 100.0;
            eprint!("\rProgresso: {progresso:.1}%");
        }
    }

    arquivo.flush().await?;
    println!("\nDownload concluído: {destino}");
    Ok(baixado)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    download_arquivo(
        "https://httpbin.org/bytes/1024000",
        "arquivo_baixado.bin"
    ).await?;
    Ok(())
}

Proxy

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let client = Client::builder()
        // Proxy HTTP
        .proxy(Proxy::http("http://proxy.exemplo.com:8080")?)
        // Proxy HTTPS
        .proxy(Proxy::https("https://proxy.exemplo.com:8080")?)
        // Ou proxy para tudo
        .proxy(Proxy::all("http://proxy.exemplo.com:8080")?)
        .build()?;

    let r = client.get("https://httpbin.org/ip").send().await?;
    println!("{}", r.text().await?);
    Ok(())
}

Requisições Concorrentes

use futures_util::future::join_all;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Cep {
    cep: String,
    logradouro: String,
    bairro: String,
    localidade: String,
    uf: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let ceps = vec!["01001000", "20040020", "30130000", "40020000"];

    // Criar futures para todas as requisições
    let futures: Vec<_> = ceps.iter().map(|cep| {
        let client = client.clone();
        let url = format!("https://viacep.com.br/ws/{cep}/json/");
        async move {
            client.get(&url)
                .send()
                .await?
                .json::<Cep>()
                .await
        }
    }).collect();

    // Executar todas concorrentemente
    let resultados = join_all(futures).await;

    for resultado in resultados {
        match resultado {
            Ok(cep) => println!("{}: {}, {} - {}", cep.cep, cep.logradouro, cep.localidade, cep.uf),
            Err(e) => eprintln!("Erro: {e}"),
        }
    }

    Ok(())
}

Requisições Bloqueantes (Síncronas)

Para scripts simples ou quando não há runtime async:

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

#[derive(Debug, Deserialize)]
struct Ip {
    origin: String,
}

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

    let ip: Ip = client
        .get("https://httpbin.org/ip")
        .send()?
        .json()?;

    println!("Meu IP: {}", ip.origin);
    Ok(())
}

Boas Práticas

  1. Sempre reutilize o Client — ele mantém um pool de conexões TCP
  2. Configure timeouts — nunca deixe requisições sem timeout em produção
  3. Implemente retry com backoff exponencial para resiliência
  4. Use error_for_status() para converter erros HTTP automaticamente
  5. Streaming para arquivos grandes — evite carregar tudo na memória
  6. Use json() do Serde para serializar/deserializar automaticamente
// Padrão recomendado: client global com Arc
use std::sync::Arc;

struct AppState {
    http_client: reqwest::Client,
}

impl AppState {
    fn new() -> Self {
        Self {
            http_client: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(30))
                .connect_timeout(std::time::Duration::from_secs(10))
                .pool_max_idle_per_host(20)
                .build()
                .unwrap(),
        }
    }
}

Conclusão

O Reqwest é a escolha definitiva para requisições HTTP em Rust. Sua API ergonômica, integração com Serde e Tokio, e suporte a features avançadas como streaming, proxy e cookies cobrem virtualmente qualquer cenário de HTTP client. Combinado com tratamento de erros robusto e retry, você tem uma base sólida para consumir qualquer API.

Veja Também