HTTP client com reqwest

1. Introdução ao reqwest

O reqwest é a biblioteca HTTP client mais popular do ecossistema Rust, oferecendo uma API ergonômica, assíncrona e type-safe para realizar requisições HTTP. Construída sobre o hyper e tokio, ela fornece suporte nativo a TLS (via rustls ou native-tls), cookies, redirecionamentos e compressão.

Principais características:
- Totalmente assíncrono (baseado em async/await)
- Tipos seguros para headers, status codes e corpos
- Suporte integrado a JSON, formulários e multipart
- Pool de conexões reutilizável

Instalação: Adicione ao Cargo.toml:

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

2. Requisições Básicas (GET e POST)

GET simples

use reqwest::Error;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let response = reqwest::get("https://api.github.com/repos/rust-lang/rust")
        .await?;

    println!("Status: {}", response.status());
    println!("Headers: {:?}", response.headers());

    let body = response.text().await?;
    println!("Body: {}", body);

    Ok(())
}

POST com JSON

use serde::Serialize;

#[derive(Serialize)]
struct NovoUsuario {
    nome: String,
    email: String,
}

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let usuario = NovoUsuario {
        nome: "João".into(),
        email: "joao@exemplo.com".into(),
    };

    let client = reqwest::Client::new();
    let response = client
        .post("https://api.exemplo.com/usuarios")
        .json(&usuario)
        .send()
        .await?;

    println!("Criado: {}", response.status().is_success());
    Ok(())
}

Formulários e Multipart

use reqwest::multipart;

// Formulário URL-encoded
let form = [("campo1", "valor1"), ("campo2", "valor2")];
let response = client
    .post("https://httpbin.org/post")
    .form(&form)
    .send()
    .await?;

// Multipart para upload de arquivos
let form = multipart::Form::new()
    .text("nome", "foto")
    .file("arquivo", "caminho/para/imagem.jpg")?;

let response = client
    .post("https://api.exemplo.com/upload")
    .multipart(form)
    .send()
    .await?;

3. Configuração Avançada do Cliente

Para performance e controle fino, crie um Client reutilizável:

use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};

fn criar_cliente() -> reqwest::Client {
    let mut headers = HeaderMap::new();
    headers.insert(USER_AGENT, HeaderValue::from_static("meu-app/1.0"));
    headers.insert("X-Custom-Header", HeaderValue::from_static("valor"));

    reqwest::Client::builder()
        .timeout(Duration::from_secs(30))
        .connect_timeout(Duration::from_secs(10))
        .default_headers(headers)
        .pool_max_idle_per_host(10)  // pool de conexões
        .tcp_keepalive(Duration::from_secs(60))
        .build()
        .expect("Falha ao criar cliente HTTP")
}

4. Serialização e Desserialização com Serde

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
struct Repo {
    name: String,
    description: Option<String>,
    stargazers_count: u32,
}

#[derive(Serialize)]
struct SearchQuery {
    q: String,
    per_page: u8,
}

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

    // Desserialização automática
    let repos: Vec<Repo> = client
        .get("https://api.github.com/search/repositories")
        .query(&SearchQuery { q: "rust".into(), per_page: 5 })
        .send()
        .await?
        .json()
        .await?;

    for repo in &repos {
        println!("{} - ⭐ {}", repo.name, repo.stargazers_count);
    }

    Ok(())
}

5. Autenticação e Headers Customizados

Bearer Token

use reqwest::header::{AUTHORIZATION, HeaderValue};

async fn api_autenticada(token: &str) -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();

    let response = client
        .get("https://api.github.com/user")
        .header(AUTHORIZATION, format!("Bearer {}", token))
        .send()
        .await?;

    println!("Autenticado: {}", response.status().is_success());
    Ok(())
}

Autenticação Básica

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

    let response = client
        .get("https://api.exemplo.com/protegido")
        .basic_auth("usuario", Some("senha"))
        .send()
        .await?;

    Ok(())
}

6. Upload e Download de Arquivos

Download com Streaming

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

async fn download_arquivo(url: &str, caminho: &str) -> Result<(), reqwest::Error> {
    let response = reqwest::get(url).await?;
    let mut file = File::create(caminho).await.unwrap();

    let mut stream = response.bytes_stream();
    use futures_util::StreamExt;

    while let Some(chunk) = stream.next().await {
        let chunk = chunk?;
        file.write_all(&chunk).await.unwrap();
    }

    println!("Download concluído!");
    Ok(())
}

Upload com Monitoramento

use reqwest::multipart::Form;
use indicatif::{ProgressBar, ProgressStyle};

async fn upload_com_progresso() -> Result<(), reqwest::Error> {
    let pb = ProgressBar::new(1024 * 1024); // 1MB
    pb.set_style(ProgressStyle::default_bar()
        .template("{msg} {bar:40} {bytes}/{total_bytes}")
        .unwrap());

    let form = Form::new()
        .part("arquivo", reqwest::Body::from(
            tokio::fs::read("dados.bin").await.unwrap()
        ));

    let client = reqwest::Client::new();
    let response = client
        .post("https://api.exemplo.com/upload")
        .multipart(form)
        .send()
        .await?;

    pb.finish_with_message("Upload concluído!");
    println!("Resposta: {}", response.status());
    Ok(())
}

7. Tratamento de Erros e Retry

Categorias de Erro

async fn tratar_erros() -> Result<(), reqwest::Error> {
    let result = reqwest::get("https://api.exemplo.com").await;

    match result {
        Ok(response) => {
            if response.status().is_success() {
                println!("Sucesso!");
            } else if response.status().is_server_error() {
                eprintln!("Erro 5xx: {}", response.status());
            } else if response.status().is_client_error() {
                eprintln!("Erro 4xx: {}", response.status());
            }
        }
        Err(e) => {
            if e.is_timeout() {
                eprintln!("Timeout na requisição");
            } else if e.is_connect() {
                eprintln!("Falha de conexão");
            } else {
                eprintln!("Erro desconhecido: {}", e);
            }
        }
    }
    Ok(())
}

Retry com Backoff Exponencial

use backoff::ExponentialBackoff;
use backoff::future::retry;

async fn requisicao_com_retry() -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();
    let op = || async {
        let response = client
            .get("https://api.exemplo.com")
            .send()
            .await
            .map_err(|e| backoff::Error::Transient {
                err: e,
                retry_after: None,
            })?;

        if response.status().is_server_error() {
            return Err(backoff::Error::Transient {
                err: reqwest::Error::from(response.error_for_status().unwrap_err()),
                retry_after: None,
            });
        }

        Ok(response)
    };

    let backoff = ExponentialBackoff::default();
    let _response = retry(backoff, op).await.map_err(|e| match e {
        backoff::Error::Permanent(e) | backoff::Error::Transient { err: e, .. } => e,
    })?;

    Ok(())
}

8. Boas Práticas e Performance

Cliente Global Reutilizável

use once_cell::sync::OnceCell;
use reqwest::Client;

static HTTP_CLIENT: OnceCell<Client> = OnceCell::new();

fn get_client() -> &'static Client {
    HTTP_CLIENT.get_or_init(|| {
        Client::builder()
            .gzip(true)
            .brotli(true)
            .redirect(reqwest::redirect::Policy::limited(10))
            .build()
            .expect("Falha ao criar cliente HTTP")
    })
}

// Em handlers Axum/Actix
async fn handler() -> impl axum::response::IntoResponse {
    let response = get_client()
        .get("https://api.exemplo.com")
        .send()
        .await;
    // ...
}

Testes com Mock Servers

use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};

#[tokio::test]
async fn test_api_externa() {
    let mock_server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/usuarios"))
        .respond_with(ResponseTemplate::new(200)
            .set_body_json(serde_json::json!([{"id": 1, "nome": "Teste"}])))
        .mount(&mock_server)
        .await;

    let client = reqwest::Client::new();
    let response = client
        .get(format!("{}/usuarios", &mock_server.uri()))
        .send()
        .await
        .unwrap();

    assert_eq!(response.status(), 200);
    let usuarios: Vec<serde_json::Value> = response.json().await.unwrap();
    assert_eq!(usuarios.len(), 1);
}

Resumo

O reqwest oferece uma experiência completa para consumo de APIs HTTP em Rust, combinando segurança de tipos com performance assíncrona. Desde requisições simples até streaming de arquivos e retry automático, a biblioteca se adapta desde prototipação rápida até sistemas de produção.

Pontos-chave:
- Sempre reutilize o Client para melhor performance
- Utilize serde para serialização/desserialização automática
- Configure timeouts e pool de conexões para evitar vazamentos
- Implemente retry com backoff para resiliência
- Teste com wiremock para simular APIs externas

Referências