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
Neste artigo

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.

Perguntas frequentes

Como configurar um proxy HTTP com autenticação no reqwest Rust?

Use reqwest::Proxy::all() com a URL no formato http://username:password@host:port. Para ProxyHat, a URL seria http://user-country-US:password@gate.proxyhat.com:8080. O país e a sessão são passados diretamente no username.

Qual a diferença entre rustls e native-tls no reqwest?

rustls é uma implementação TLS pura em Rust, ideal para containers e cross-compilation. native-tls usa OpenSSL (ou Security.framework no macOS) e é melhor para ambientes corporativos com CAs customizados. Alterne entre eles via feature flags no Cargo.toml.

Como fazer rotação de IPs em Rust com proxies residenciais?

Crie um pool de proxies com diferentes parâmetros de geo-targeting e sessão no username. Use um trait ProxyPool com métodos next_proxy(), mark_failed() e mark_success(). O reqwest cria um novo Client por proxy, e o pool gerencia a rotação e remoção de proxies falhos.

Como usar SOCKS5 com reqwest em Rust?

Habilite a feature "socks" no reqwest no Cargo.toml e use Proxy::all() com o esquema socks5://. Exemplo: socks5://user-country-BR:password@gate.proxyhat.com:1080. A porta SOCKS5 do ProxyHat é 1080.

Como fazer scraping concorrente com proxies em Rust?

Use tokio::task::JoinSet para gerenciar tarefas concorrentes com backpressure. Configure pool_max_idle_per_host no reqwest igual ao nível de concorrência desejado e processe URLs em batches para respeitar rate limits do proxy e do site alvo.

Pronto para começar?

Proxies residenciais, ISP e móveis em mais de 148 países. Crie uma conta grátis.

Criar conta grátis
← Voltar ao Blog