Por que usar Rust HTTP proxy para scraping de alta performance
Se você está construindo infraestrutura de scraping em Rust, já sabe que a linguagem oferece segurança de memória e concorrência sem overhead de runtime. Mas ao adicionar Rust HTTP proxy à stack, você desbloqueia acesso a dados que bloqueiam IPs de datacenter, contorna rate limits e escala horizontalmente sem ser banido.
O problema: a maioria dos tutoriais mostra apenas o básico do reqwest com proxy. Na prática, você precisa de autenticação, rotação de IPs, TLS configurável, tratamento de erros robusto e concorrência real. Este guia cobre tudo isso com código executável.
Configuração do projeto e feature flags
Antes de escrever código, configure as dependências corretamente. O ecossistema Rust permite escolher entre native-tls e rustls em tempo de compilação — uma decisão que afeta portabilidade, tamanho do binário e compatibilidade com proxies corporativos.
[package]
name = "proxy-scraper"
edition = "2021"
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls", # ou "native-tls" para OpenSSL
"proxy", # suporte a proxy HTTP/SOCKS5
"json",
"cookies",
] }
tokio = { version = "1", features = ["full"] }
hyper = { version = "1", features = ["client", "http1"] }
hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1"] }
hyper-proxy = { version = "0.1" }
tower = "0.4"
thiserror = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
rand = "0.8"
[features]
default = ["rustls-backend"]
rustls-backend = ["reqwest/rustls-tls"]
native-tls-backend = ["reqwest/native-tls"]
socks5 = ["reqwest/socks"]
Nota sobre feature flags: Desabilitar
default-featuresno reqwest e selecionar manualmente o backend TLS reduz o binário final em até 2-3 MB e elimina dependências desnecessárias do OpenSSL em builds estáticos.
reqwest com proxy: configuração básica e autenticação
O reqwest é o cliente HTTP mais usado em Rust. Configurar um reqwest proxy é direto, mas a autenticação e o geo-targeting exigem atenção ao formato da URL.
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Proxy com autenticação básica e geo-targeting por país
let proxy_url = "http://user-country-US:password@gate.proxyhat.com:8080";
let proxy = Proxy::all(proxy_url)?;
let client = reqwest::Client::builder()
.proxy(proxy)
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.timeout(std::time::Duration::from_secs(30))
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?;
let data: serde_json::Value = resp.json().await?;
println!("IP via proxy: {}", data["origin"]);
Ok(())
}
Para Rust residential proxies com sticky sessions (IP fixo por sessão), use o flag de sessão no username:
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Sticky session: o mesmo IP é mantido enquanto a sessão durar
let session_id = format!("session-{}", uuid::Uuid::new_v4());
let proxy_url = format!(
"http://user-country-DE-{}-city-berlin:password@gate.proxyhat.com:8080",
session_id
);
let proxy = Proxy::all(&proxy_url)?;
let client = reqwest::Client::builder()
.proxy(proxy)
.cookie_store(true) // mantém cookies durante a sessão
.build()?;
// Múltiplas requisições com o mesmo IP de saída
for i in 0..5 {
let resp = client.get("https://httpbin.org/ip").send().await?;
let text = resp.text().await?;
println!("Requisição {}: {}", i + 1, text.trim());
}
Ok(())
}
SOCKS5 como alternativa
Quando o tráfego HTTP precisa ser encapsulado com mais opacidade, use SOCKS5 na porta 1080:
// Requer feature "socks5" habilitada no Cargo.toml
let proxy = Proxy::all("socks5://user-country-BR:password@gate.proxyhat.com:1080")?;
let client = reqwest::Client::builder().proxy(proxy).build()?;
hyper com proxy-connect para HTTPS via HTTP proxy
Para controle de baixo nível — custom DNS resolution, inspection do CONNECT tunnel, ou integração com middleware tower — o hyper é a escolha certa. Ele não tem proxy built-in; você implementa o CONNECT manualmente.
use hyper::Request;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Cliente HTTP/1.1 sem proxy (conexão direta ao proxy)
let proxy_client = Client::builder(TokioExecutor::new())
.http1_only()
.build_http::<String>();
// Enviar CONNECT para o proxy, pedindo túnel até o destino
let target = "httpbin.org:443";
let connect_req = Request::builder()
.method("CONNECT")
.uri(target)
.header(
"Proxy-Authorization",
format!(
"Basic {}",
base64::encode("user-country-GB:password")
),
)
.body(String::new())?;
// Conectar ao proxy em gate.proxyhat.com:8080
let proxy_stream = tokio::net::TcpStream::connect("gate.proxyhat.com:8080").await?;
let (mut sender, conn) = hyper::client::conn::http1::handshake(proxy_stream).await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("Erro na conexão: {}", e);
}
});
// Enviar CONNECT
let response = sender.send_request(connect_req).await?;
if response.status() != 200 {
return Err(format!("CONNECT falhou: {}", response.status()).into());
}
println!("Túnel CONNECT estabelecido para {}", target);
// A partir daqui, o stream pode ser usado para TLS handshake
// com o destino final via rustls ou native-tls
Ok(())
}
Quando usar hyper direto: Quando você precisa interceptar o handshake CONNECT, implementar custom DNS, ou construir um middleware tower que reqwest não suporta nativamente.
tokio + JoinSet para scraping concorrente
Scraping real exige centenas de requisições em paralelo. O JoinSet do tokio gerencia tarefas concorrentes com backpressure natural — ao contrário de spawn descontrolado.
use reqwest::Proxy;
use std::sync::Arc;
use tokio::task::JoinSet;
use std::error::Error;
struct ScraperConfig {
client: reqwest::Client,
concurrency: usize,
}
impl ScraperConfig {
fn new(proxy_url: &str, concurrency: usize) -> Result<Self, Box<dyn Error>> {
let proxy = Proxy::all(proxy_url)?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(15))
.pool_max_idle_per_host(concurrency) // reutiliza conexões
.build()?;
Ok(Self { client, concurrency })
}
}
async fn fetch_url(client: &reqwest::Client, url: &str) -> Result<String, reqwest::Error> {
client
.get(url)
.send()
.await?
.text()
.await
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let config = ScraperConfig::new(
"http://user-country-US:password@gate.proxyhat.com:8080",
50,
)?;
let config = Arc::new(config);
let urls: Vec<String> = (0..200)
.map(|i| format!("https://httpbin.org/get?id={}", i))
.collect();
let mut tasks = JoinSet::new();
let urls = Arc::new(urls);
// Processar em batches para respeitar rate limits
for chunk in urls.chunks(config.concurrency) {
for url in chunk {
let client = config.client.clone();
let url = url.clone();
tasks.spawn(async move {
match fetch_url(&client, &url).await {
Ok(body) => Some(body.len()),
Err(e) => {
eprintln!("Erro em {}: {}", url, e);
None
}
}
});
}
// Aguardar o batch completar antes de iniciar o próximo
while let Some(result) = tasks.join_next().await {
if let Ok(Some(len)) = result {
println!("Resposta recebida: {} bytes", len);
}
}
}
Ok(())
}
Abstração de pool de proxies rotativos com trait
Em produção, você não codifica uma URL de proxy. Você precisa de um pool que rotacione IPs, remova proxies falhos e respeite rate limits. Um trait em Rust define o contrato; implementações concretas variam.
use rand::seq::SliceRandom;
use std::sync::Arc;
use tokio::sync::RwLock;
use thiserror::Error;
// ── Erros customizados ──────────────────────────────────────
#[derive(Error, Debug)]
pub enum ProxyError {
#[error("Pool de proxies vazio — nenhum proxy disponível")]
PoolExhausted,
#[error("Falha ao criar cliente com proxy {proxy}: {source}")]
ClientBuild {
proxy: String,
#[source]
source: reqwest::Error,
},
#[error("Todas as tentativas falharam para {url}")]
AllRetriesFailed { url: String },
}
// ── Trait do pool de proxies ────────────────────────────────
#[async_trait::async_trait]
pub trait ProxyPool: Send + Sync {
async fn next_proxy(&self) -> Result<String, ProxyError>;
async fn mark_failed(&self, proxy_url: &str);
async fn mark_success(&self, proxy_url: &str);
}
// ── Implementação com rotação round-robin + sticky sessions ──
pub struct RotatingPool {
proxies: Arc<RwLock<Vec<ProxyEntry>>>,
index: Arc<RwLock<usize>>,
}
struct ProxyEntry {
url: String,
fail_count: u32,
success_count: u32,
}
impl RotatingPool {
pub fn new(countries: &[&str], password: &str) -> Self {
let proxies: Vec<ProxyEntry> = countries
.iter()
.map(|&c| ProxyEntry {
url: format!(
"http://user-country-{}-session-{}:{}@gate.proxyhat.com:8080",
c,
uuid::Uuid::new_v4(),
password
),
fail_count: 0,
success_count: 0,
})
.collect();
Self {
proxies: Arc::new(RwLock::new(proxies)),
index: Arc::new(RwLock::new(0)),
}
}
}
#[async_trait::async_trait]
impl ProxyPool for RotatingPool {
async fn next_proxy(&self) -> Result<String, ProxyError> {
let proxies = self.proxies.read().await;
if proxies.is_empty() {
return Err(ProxyError::PoolExhausted);
}
let mut idx = self.index.write().await;
let proxy = proxies[*idx % proxies.len()].url.clone();
*idx += 1;
Ok(proxy)
}
async fn mark_failed(&self, proxy_url: &str) {
let mut proxies = self.proxies.write().await;
if let Some(entry) = proxies.iter_mut().find(|p| p.url == proxy_url) {
entry.fail_count += 1;
// Remover proxy com mais de 5 falhas consecutivas
if entry.fail_count >= 5 {
eprintln!("Removendo proxy falho: {}", proxy_url);
}
}
}
async fn mark_success(&self, proxy_url: &str) {
let mut proxies = self.proxies.write().await;
if let Some(entry) = proxies.iter_mut().find(|p| p.url == proxy_url) {
entry.success_count += 1;
entry.fail_count = 0; // resetar falhas consecutivas
}
}
}
// ── Cliente com retry automático ────────────────────────────
pub struct ProxyClient {
pool: Arc<dyn ProxyPool>,
max_retries: usize,
}
impl ProxyClient {
pub fn new(pool: Arc<dyn ProxyPool>, max_retries: usize) -> Self {
Self { pool, max_retries }
}
pub async fn get(&self, url: &str) -> Result<String, ProxyError> {
for attempt in 0..self.max_retries {
let proxy_url = self.pool.next_proxy().await?;
let proxy = reqwest::Proxy::all(&proxy_url)
.map_err(|e| ProxyError::ClientBuild {
proxy: proxy_url.clone(),
source: e,
})?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()
.map_err(|e| ProxyError::ClientBuild {
proxy: proxy_url.clone(),
source: e,
})?;
match client.get(url).send().await {
Ok(resp) => {
self.pool.mark_success(&proxy_url).await;
return Ok(resp.text().await.unwrap_or_default());
}
Err(e) => {
eprintln!("Tentativa {} falhou: {}", attempt + 1, e);
self.pool.mark_failed(&proxy_url).await;
}
}
}
Err(ProxyError::AllRetriesFailed {
url: url.to_string(),
})
}
}
Tratamento de erros com thiserror
Já introduzimos o ProxyError acima. O padrão com thiserror garante erros tipados, mensagens claras e compatibilidade com ?. Para scraping de produção, estenda o enum com erros de parsing e rate limiting:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ScrapingError {
#[error("Erro de proxy: {0}")]
Proxy(#[from] ProxyError),
#[error("Erro HTTP: {0}")]
Http(#[from] reqwest::Error),
#[error("Rate limit atingido para {domain} — retry após {retry_after}s")]
RateLimited {
domain: String,
retry_after: u64,
},
#[error("Resposta inválida de {url}: status {status}")]
InvalidResponse {
url: String,
status: u16,
},
#[error("Timeout após {timeout_ms}ms para {url}")]
Timeout {
url: String,
timeout_ms: u64,
},
}
// Conversão manual de reqwest::Error para capturar rate limits
impl From<reqwest::Error> for ScrapingError {
fn from(e: reqwest::Error) -> Self {
if let Some(status) = e.status() {
if status.as_u16() == 429 {
return ScrapingError::RateLimited {
domain: e.url()
.and_then(|u| u.host_str().map(String::from))
.unwrap_or_default(),
retry_after: 60, // valor padrão; parse do header em produção
};
}
return ScrapingError::InvalidResponse {
url: e.url().map(|u| u.to_string()).unwrap_or_default(),
status: status.as_u16(),
};
}
ScrapingError::Http(e)
}
}
rustls vs native-tls: qual escolher?
A escolha do backend TLS afeta portabilidade, tamanho do binário e compatibilidade com ambientes restritos. Aqui está a comparação direta:
| Critério | rustls | native-tls (OpenSSL) |
|---|---|---|
| Tamanho do binário | Menor (~2 MB a menos) | Maior (linka OpenSSL) |
| Cross-compile | Simples (puro Rust) | Complexo (toolchain OpenSSL) |
| Compatibilidade corporativa | Pode falhar com CAs customizados | Usa keystore do SO |
| Performance | Comparável; melhor em handshakes | Maduro; otimizado por décadas |
| Auditoria de segurança | Escrita em Rust; memory-safe | Código C; histórico de CVEs |
| ALPN / HTTP2 | Suporte completo | Suporte completo |
Recomendação: Use rustls-tls para containers Docker e builds estáticos. Use native-tls em ambientes corporativos com CAs customizados ou quando precisar de FIPS compliance.
Alternar é trivial com feature flags — já configuramos isso no Cargo.toml. Basta compilar com:
# Build com rustls (padrão)
cargo build --release
# Build com native-tls
cargo build --release --no-default-features --features native-tls-backend
# Build com suporte SOCKS5
cargo build --release --features socks5
Feature flags condicionais para proxy support
Em bibliotecas Rust, é comum expor proxy como feature opcional. Isso permite que consumidores que não precisam de proxy compilam um binário menor:
// lib.rs
#[cfg(feature = "proxy")]
pub fn create_proxy_client(proxy_url: &str) -> Result<reqwest::Client, ProxyError> {
let proxy = reqwest::Proxy::all(proxy_url)
.map_err(|e| ProxyError::ClientBuild {
proxy: proxy_url.to_string(),
source: e,
})?;
reqwest::Client::builder()
.proxy(proxy)
.build()
.map_err(|e| ProxyError::ClientBuild {
proxy: proxy_url.to_string(),
source: e,
})
}
#[cfg(not(feature = "proxy"))]
pub fn create_proxy_client(_proxy_url: &str) -> Result<reqwest::Client, ProxyError> {
// Sem proxy — cliente direto
reqwest::Client::builder()
.build()
.map_err(|e| ProxyError::ClientBuild {
proxy: "direct".to_string(),
source: e,
})
}
Dicas de produção para scraping com proxies em Rust
- Connection pooling: O reqwest mantém um pool interno. Configure
pool_max_idle_per_hostigual à sua concorrência para evitar re-criar conexões TCP/TLS. - Timeouts progressivos: Use timeout curto (10s) para dados quentes e longo (30s) para páginas pesadas. Implemente retry com backoff exponencial.
- Respeite robots.txt e ToS: Mesmo com proxies, scraping agressivo pode violar termos de serviço. Consulte nosso guia de ética e legalidade.
- Logging estruturado: Use
tracingcom spans que incluem proxy_url, status code e latência. Isso é indispensável para debugar falhas em pools com centenas de proxies. - Geo-targeting granular: Com ProxyHat, você pode segmentar por país e cidade — ideal para SERP tracking e price monitoring regional.
Pontos-chave
- Use reqwest para 90% dos casos — ele suporta proxy HTTP e SOCKS5 com autenticação nativa.
- Use hyper quando precisar de controle sobre o CONNECT tunnel ou middleware tower customizado.
- JoinSet do tokio oferece backpressure natural para scraping concorrente — prefira sobre
spawndescontrolado.- Abstraia o pool de proxies com um trait — isso permite trocar estratégias (round-robin, aleatório, ponderado) sem mudar o código de scraping.
- Use thiserror para erros tipados que funcionam com
?e produzem mensagens claras em logs.- Escolha rustls para containers e native-tls para ambientes corporativos — alterne com feature flags.
- Sempre configure timeouts, retries e logging estruturado antes de ir para produção.
Pronto para escalar seu scraping com Rust residential proxies? Confira os planos do ProxyHat e comece com uma pool de proxies em 190+ países — configurável diretamente no username, sem SDK extra.






