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
- Sempre reutilize o
Client— ele mantém um pool de conexões TCP - Configure timeouts — nunca deixe requisições sem timeout em produção
- Implemente retry com backoff exponencial para resiliência
- Use
error_for_status()para converter erros HTTP automaticamente - Streaming para arquivos grandes — evite carregar tudo na memória
- 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.