Guia Completo de Rust HTTP Proxy: reqwest, hyper e Rotação de IPs

Domine o uso de proxies HTTP em Rust com reqwest, hyper e tokio. Configure autenticação, TLS, rotação de IPs e scraping concorrente com exemplos práticos usando ProxyHat.

Guia Completo de Rust HTTP Proxy: reqwest, hyper e Rotação de IPs

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-features no 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_host igual à 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 tracing com 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 spawn descontrolado.
  • 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.

Pronto para começar?

Acesse mais de 50M de IPs residenciais em mais de 148 países com filtragem por IA.

Ver preçosProxies residenciais
← Voltar ao Blog