Rust HTTP Proxy: Guida Completa con reqwest, hyper e Pool Rotante

Scopri come configurare proxy HTTP in Rust con reqwest, hyper, tokio e un pool rotante. Codice pronto per produzione, TLS, concorrenza e residential proxies.

Rust HTTP Proxy: Guida Completa con reqwest, hyper e Pool Rotante

Perché usare un Rust HTTP proxy nei tuoi progetti

Se stai costruendo infrastruttura di scraping o automazione in Rust, prima o poi ti scontri con un problema: un singolo IP non basta. Rate limiting, geoblocking e CAPTCHA rendono impossibile scalare senza un layer di proxy. Rust è il linguaggio ideale per questo lavoro — velocità, sicurezza di memoria e async/await nativo — ma l'ecosistema proxy è frammentato e la documentazione è sparsa.

In questa guida copriamo tutto ciò che serve per andare da zero a un sistema di scraping concorrente con reqwest proxy, hyper low-level, pool rotante via trait, error handling robusto e TLS configurabile. Ogni blocco di codice è eseguibile e usa i parametri di connessione ProxyHat.

Configurare reqwest con proxy e autenticazione

reqwest è il client HTTP de facto in Rust. Supporta proxy HTTP/SOCKS out of the box tramite feature flags. Il primo passo è configurare il Cargo.toml con le feature giuste:

[dependencies]
reqwest = { version = "0.12", features = ["proxy", "rustls-tls"], default-features = false }
tokio = { version = "1", features = ["full"] }

Ora un esempio completo che configura un proxy residential con autenticazione e geo-targeting:

use reqwest::Proxy;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Proxy con autenticazione e geo-targeting (Italia, Roma)
    let proxy_url = "http://user-country-IT-city-rome: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)")
        .build()?;

    let resp = client
        .get("https://httpbin.org/ip")
        .send()
        .await?;

    let body = resp.text().await?;
    println!("IP tramite proxy: {}", body);
    Ok(())
}

Nota come il geo-targeting venga passato direttamente nello username: user-country-IT-city-rome. Questo è il pattern ProxyHat — nessun header extra, tutto nella stringa di autenticazione.

Sessioni sticky per richieste correlate

Quando devi mantenere la stessa IP per una sessione di login o un flusso multi-step, usa il flag session:

use reqwest::Proxy;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Sessione sticky: l'IP rimane coerente per tutte le richieste
    let proxy_url = "http://user-session-abc123:PASSWORD@gate.proxyhat.com:8080";

    let proxy = Proxy::all(proxy_url)?;
    let client = reqwest::Client::builder()
        .proxy(proxy)
        .cookie_store(true) // mantiene i cookie tra richieste
        .build()?;

    // Prima richiesta: login
    let login = client
        .post("https://example.com/login")
        .form(&[("user", "myuser"), ("pass", "mypass")])
        .send()
        .await?;
    println!("Login status: {}", login.status());

    // Seconda richiesta: stessa sessione, stesso IP
    let dashboard = client
        .get("https://example.com/dashboard")
        .send()
        .await?;
    println!("Dashboard status: {}", dashboard.status());
    Ok(())
}

hyper: controllo low-level con proxy-connect

Quando reqwest è troppo astratto — per esempio se devi manipolare il CONNECT tunnel manualmente o gestire pool di connessioni custom — hyper è il livello giusto. Il crate hyper-proxy gestisce il CONNECT method per HTTPS tramite proxy HTTP.

use hyper::{Body, Client, Request, Uri};
use hyper_proxy::{Proxy, ProxyConnector, Intercept};
use hyper_rustls::HttpsConnector;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Costruisci il connector HTTPS con rustls
    let https = HttpsConnector::new();

    // Configura il proxy
    let proxy_uri: Uri = "http://user:PASSWORD@gate.proxyhat.com:8080".parse()?;
    let proxy = Proxy::new(Intercept::All, proxy_uri);

    let connector = ProxyConnector::from_proxy(https, proxy)?;

    let client = Client::builder().build(connector);

    let req = Request::builder()
        .uri("https://httpbin.org/ip")
        .header("User-Agent", "RustScraper/1.0")
        .body(Body::empty())?;

    let resp = client.request(req).await?;
    println!("Status: {}", resp.status());

    let body = hyper::body::to_bytes(resp.into_body()).await?;
    println!("Body: {}", String::from_utf8_lossy(&body));
    Ok(())
}

Con hyper hai controllo totale sul CONNECT tunnel, sui timeout per singola connessione e sul retry a livello di trasporto. È più verboso, ma essenziale per casi d'uso avanzati come Rust residential proxies con rotazione per dominio.

Scraping concorrente con tokio e JoinSet

Il vero vantaggio di Rust è la concorrenza zero-cost. Con tokio::task::JoinSet puoi lanciare centinaia di richieste concorrenti con backpressure naturale — il set limita il numero di task attivi simultaneamente quando lo gestisci correttamente.

use reqwest::Proxy;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://httpbin.org/ip",
        "https://httpbin.org/headers",
        "https://httpbin.org/user-agent",
        "https://httpbin.org/get",
        "https://httpbin.org/uuid",
    ];

    // Client condiviso — Arc per sicurezza tra task
    let proxy_url = "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080";
    let proxy = Proxy::all(proxy_url)?;
    let client = Arc::new(
        reqwest::Client::builder()
            .proxy(proxy)
            .timeout(Duration::from_secs(15))
            .build()?
    );

    let mut tasks = JoinSet::new();

    for url in urls {
        let c = client.clone();
        tasks.spawn(async move {
            match c.get(url).send().await {
                Ok(resp) => {
                    let status = resp.status();
                    let text = resp.text().await.unwrap_or_default();
                    (url, Ok(format!("{} — {} bytes", status, text.len())))
                }
                Err(e) => (url, Err(e.to_string())),
            }
        });
    }

    // Raccogli i risultati man mano che completano
    while let Some(result) = tasks.join_next().await {
        match result {
            Ok((url, Ok(info))) => println!("✓ {}: {}", url, info),
            Ok((url, Err(e))) => eprintln!("✗ {}: {}", url, e),
            Err(e) => eprintln!("Panic nel task: {}", e),
        }
    }

    Ok(())
}

Per scalare davvero, combina JoinSet con un semaphore per limitare la concorrenza massima e proteggere sia il tuo client che il provider di proxy da sovraccarico.

Astrazione del pool rotante: un trait per tutti i proxy

Il pattern più potente per gestire Rust residential proxies è un trait che incapsula la rotazione. Così puoi swappare strategie — round-robin, random, least-recently-used — senza toccare il codice di scraping.

use async_trait::async_trait;
use reqwest::{Client, Proxy};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;

/// Trait per qualsiasi pool di proxy rotante
#[async_trait]
pub trait ProxyPool: Send + Sync {
    /// Restituisce il prossimo proxy URL da usare
    async fn next_proxy(&self) -> Result<String, PoolError>;
    /// Segnala che un proxy ha fallito (per blacklist temporanea)
    async fn mark_failed(&self, proxy_url: &str);
    /// Segnala che un proxy ha avuto successo
    async fn mark_success(&self, proxy_url: &str);
}

#[derive(Debug, thiserror::Error)]
pub enum PoolError {
    #[error("Nessun proxy disponibile — tutti in blacklist")]
    Exhausted,
    #[error("Errore di configurazione proxy: {0}")]
    Config(String),
}

/// Pool con rotazione round-robin e blacklist temporanea
pub struct RoundRobinPool {
    proxies: Vec<String>,
    index: Arc<RwLock<usize>>,
    failures: Arc<RwLock<HashMap<String, u32>>>,
    max_failures: u32,
}

impl RoundRobinPool {
    pub fn new(proxy_urls: Vec<String>, max_failures: u32) -> Self {
        Self {
            proxies: proxy_urls,
            index: Arc::new(RwLock::new(0)),
            failures: Arc::new(RwLock::new(HashMap::new())),
            max_failures,
        }
    }
}

#[async_trait]
impl ProxyPool for RoundRobinPool {
    async fn next_proxy(&self) -> Result<String, PoolError> {
        let failures = self.failures.read().await;
        let mut idx = self.index.write().await;

        // Prova fino a trovare un proxy non in blacklist
        let start = *idx;
        for _ in 0..self.proxies.len() {
            let proxy = self.proxies[*idx % self.proxies.len()].clone();
            *idx = (*idx + 1) % self.proxies.len();

            let fail_count = failures.get(&proxy).copied().unwrap_or(0);
            if fail_count < self.max_failures {
                return Ok(proxy);
            }
        }

        // Se siamo qui, tutti i proxy sono in blacklist — reset
        drop(failures);
        self.failures.write().await.clear();
        Err(PoolError::Exhausted)
    }

    async fn mark_failed(&self, proxy_url: &str) {
        let mut failures = self.failures.write().await;
        *failures.entry(proxy_url.to_string()).or_insert(0) += 1;
    }

    async fn mark_success(&self, proxy_url: &str) {
        let mut failures = self.failures.write().await;
        failures.remove(proxy_url);
    }
}

/// Costruisce un client reqwest usando il prossimo proxy dal pool
pub async fn client_from_pool(pool: &dyn ProxyPool) -> Result<Client, PoolError> {
    let proxy_url = pool.next_proxy().await?;
    let proxy = Proxy::all(&proxy_url)
        .map_err(|e| PoolError::Config(e.to_string()))?;
    reqwest::Client::builder()
        .proxy(proxy)
        .build()
        .map_err(|e| PoolError::Config(e.to_string()))
}

Con questo trait puoi iniettare qualsiasi strategia — pool statico, pool dinamico da API, weighted random — e il tuo scraper non cambia di una riga. Per approfondire i pattern di scraping, vedi la nostra guida al web scraping.

Error handling robusto con thiserror

In produzione, ogni richiesta può fallire per motivi diversi: proxy down, CAPTCHA, timeout, TLS error. Un tipo errore strutturato con thiserror rende il codice gestibile:

use thiserror::Error;

#[derive(Debug, Error)]
pub enum ScraperError {
    #[error("Proxy rifiutato la connessione: {0}")]
    ProxyRefused(String),

    #[error("Timeout dopo {0}ms per URL: {1}")]
    Timeout(u64, String),

    #[error("CAPTCHA rilevata su {0}")]
    Captcha(String),

    #[error("Rate limit (HTTP {status}) su {url}")]
    RateLimit { status: u16, url: String },

    #[error("Errore TLS: {0}")]
    Tls(String),

    #[error("Errore HTTP: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Pool esaurito: {0}")]
    Pool(#[from] PoolError),
}

/// Classifica una risposta HTTP in un errore specifico
pub fn classify_response(
    resp: &reqwest::Response,
    url: &str,
) -> Result<(), ScraperError> {
    let status = resp.status().as_u16();
    match status {
        429 => Err(ScraperError::RateLimit { status, url: url.into() }),
        403 => {
            // Controlla se è CAPTCHA dal body
            Err(ScraperError::Captcha(url.into()))
        }
        502 | 503 | 504 => Err(ScraperError::ProxyRefused(
            format!("HTTP {} — proxy o upstream down", status)
        )),
        _ if status >= 400 => Err(ScraperError::Http(
            reqwest::Error::from(resp.error_for_status_ref().unwrap_err())
        )),
        _ => Ok(()),
    }
}

/// Wrapper con retry e circuit breaker
pub async fn fetch_with_retry(
    client: &reqwest::Client,
    url: &str,
    max_retries: u32,
) -> Result<String, ScraperError> {
    let mut attempts = 0;
    loop {
        attempts += 1;
        match client.get(url).send().await {
            Ok(resp) => {
                classify_response(&resp, url)?;
                let text = resp.text().await?;
                return Ok(text);
            }
            Err(e) if attempts < max_retries => {
                if e.is_timeout() {
                    eprintln!("Timeout (tentativo {}/{}), retry...", attempts, max_retries);
                }
                tokio::time::sleep(
                    std::time::Duration::from_millis(500 * attempts as u64)
                ).await;
                continue;
            }
            Err(e) => return Err(ScraperError::Http(e)),
        }
    }
}

TLS: rustls vs native-tls — quale scegliere?

La scelta del backend TLS influenza direttamente la compatibilità e le performance del tuo client proxy. Ecco un confronto:

Caratteristicarustlsnative-tls
ImplementazionePura Rust (ring / aws-lc-rs)Wrappa OpenSSL (Linux) / SChannel (Windows) / Secure Transport (macOS)
Dimensione binarioPiù piccolo, nessuna dipendenza CPiù grande, linka OpenSSL
PerformanceComparabile, migliore su ARMOttima su x86 con OpenSSL 3.x
Compatibilità certificatiNon supporta certificati legacy (MD5, SHA-1)Supporta certificati legacy tramite config OS
Cross-compilationTrivial — nessun sysroot CComplessa — serve cross-compile OpenSSL
Audit sicurezzaMemory-safe per costruzioneDipende dalla versione OpenSSL linkata
Feature reqwestrustls-tlsnative-tls

Per lo scraping moderno con Rust residential proxies, raccomandiamo rustls-tls: cross-compilazione semplice, binario piccolo e nessuna vulnerabilità memory-safety. Se però il tuo target usa certificati enterprise non standard, native-tls può essere necessario.

Configurare rustls con CA custom

Se il tuo ambiente usa certificati self-signed o una CA interna, devi aggiungerli al trust store di rustls:

use reqwest::Client;
use std::fs;

fn build_client_with_custom_ca() -> Result<Client, Box<dyn std::error::Error>> {
    let ca_pem = fs::read("/path/to/custom-ca.pem")?;
    let cert = reqwest::Certificate::from_pem(&ca_pem)?;

    let client = Client::builder()
        .add_root_certificate(cert)
        .use_rustls_tls()
        .proxy(reqwest::Proxy::all(
            "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
        )?)
        .build()?;

    Ok(client)
}

Feature flags compile-time per proxy opzionale

In un binario di produzione, potresti voler disabilitare il supporto proxy per ridurre la superficie d'attacco o la dimensione del binario. reqwest supporta questo nativamente tramite feature flags:

# Cargo.toml
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
    "rustls-tls",
    "json",
    # Abilita solo se servono proxy
    # "proxy",
] }

[features]
# Feature custom: attiva proxy solo quando necessario
default = []
with-proxy = ["reqwest/proxy"]
with-socks = ["reqwest/socks"]
full-networking = ["with-proxy", "with-socks"]

Nel codice Rust, usa #[cfg] per compilare condizionalmente:

use reqwest::Client;

pub fn build_client(proxy_url: Option<&str>) -> Result<Client, reqwest::Error> {
    let mut builder = Client::builder()
        .user_agent("RustScraper/2.0");

    #[cfg(feature = "with-proxy")]
    if let Some(url) = proxy_url {
        builder = builder.proxy(reqwest::Proxy::all(url)?);
    }

    #[cfg(not(feature = "with-proxy"))]
    if proxy_url.is_some() {
        eprintln!("Avviso: proxy URL ignorato — feature 'with-proxy' non attiva");
    }

    builder.build()
}

Compilare senza proxy: cargo build --release. Compilare con proxy: cargo build --release --features with-proxy. Questo è particolarmente utile per build embedded o container minimizzati.

Pattern di produzione: unire tutto

Ecco come combinare pool rotante, error handling e concorrenza in un flusso di scraping reale:

use async_trait::async_trait;
use reqwest::Client;
use std::sync::Arc;
use tokio::task::JoinSet;
use tokio::sync::Semaphore;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pool = Arc::new(RoundRobinPool::new(
        vec![
            "http://user-country-US:PASSWORD@gate.proxyhat.com:8080".into(),
            "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080".into(),
            "http://user-country-GB:PASSWORD@gate.proxyhat.com:8080".into(),
        ],
        3, // max fallimenti prima della blacklist
    ));

    let targets = vec![
        "https://httpbin.org/ip",
        "https://httpbin.org/headers",
        "https://httpbin.org/get",
    ];

    let semaphore = Arc::new(Semaphore::new(5)); // max 5 concorrenti
    let mut tasks = JoinSet::new();

    for url in targets {
        let pool = pool.clone();
        let sem = semaphore.clone();

        tasks.spawn(async move {
            let _permit = sem.acquire().await.unwrap();

            // Ottieni proxy e costruisci client
            let proxy_url = pool.next_proxy().await.map_err(|e| e.to_string())?;
            let proxy = reqwest::Proxy::all(&proxy_url)
                .map_err(|e| e.to_string())?;
            let client = Client::builder()
                .proxy(proxy)
                .timeout(Duration::from_secs(10))
                .build()
                .map_err(|e| e.to_string())?;

            // Richiesta con retry
            for attempt in 1..=3 {
                match client.get(url).send().await {
                    Ok(resp) if resp.status().is_success() => {
                        pool.mark_success(&proxy_url).await;
                        let body = resp.text().await.map_err(|e| e.to_string())?;
                        return Ok::<String, String>(format!("{}: {} bytes", url, body.len()));
                    }
                    Ok(resp) => {
                        eprintln!("HTTP {} su {} (tentativo {}/3)",
                            resp.status(), url, attempt);
                        pool.mark_failed(&proxy_url).await;
                    }
                    Err(e) => {
                        eprintln!("Errore: {} (tentativo {}/3)", e, attempt);
                        pool.mark_failed(&proxy_url).await;
                    }
                }
                tokio::time::sleep(Duration::from_millis(300 * attempt as u64)).await;
            }
            Err(format!("Fallito dopo 3 tentativi: {}", url))
        });
    }

    while let Some(res) = tasks.join_next().await {
        match res {
            Ok(Ok(msg)) => println!("✓ {}", msg),
            Ok(Err(e)) => eprintln!("✗ {}", e),
            Err(e) => eprintln!("Panic: {}", e),
        }
    }

    Ok(())
}

Considerazioni etiche e legali

Usare proxy non ti esime dal rispetto di robots.txt, termini di servizio, GDPR e CCPA. Ecco le regole base:

  • Controlla sempre robots.txt prima di scrapare un dominio.
  • Rispetta i rate limit del target — anche con proxy, bombardare un server è abuso.
  • Non raccogliere dati personali senza base legale.
  • Usa residential proxies solo per accesso geografico legittimo, non per impersonare utenti reali.
  • Documenta le tue fonti e lo scopo della raccolta dati.

Key Takeaways

  • reqwest è sufficiente per il 90% dei casi — configurazione proxy in 3 righe.
  • hyper serve quando hai bisogno di controllo sul CONNECT tunnel o pool di connessioni custom.
  • Usa JoinSet + Semaphore per concorrenza controllata senza sovraccaricare i proxy.
  • Il trait ProxyPool disaccoppia la strategia di rotazione dallo scraper — cambia pool senza riscrivere.
  • thiserror rende gli errori actionable: classifica 429, CAPTCHA, timeout separatamente.
  • rustls per cross-compilation e sicurezza; native-tls per certificati legacy.
  • Feature flags compile-time riducono la superficie d'attacco quando il proxy non serve.

Pronto a costruire la tua infrastruttura di scraping in Rust? Esplora le soluzioni proxy ProxyHat o consulta le locazioni disponibili per il geo-targeting. Per approfondire il tracking SERP, vedi il nostro caso d'uso SERP tracking.

Pronto per iniziare?

Accedi a oltre 50M di IP residenziali in oltre 148 paesi con filtraggio AI.

Vedi i prezziProxy residenziali
← Torna al Blog