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:
| Caratteristica | rustls | native-tls |
|---|---|---|
| Implementazione | Pura Rust (ring / aws-lc-rs) | Wrappa OpenSSL (Linux) / SChannel (Windows) / Secure Transport (macOS) |
| Dimensione binario | Più piccolo, nessuna dipendenza C | Più grande, linka OpenSSL |
| Performance | Comparabile, migliore su ARM | Ottima su x86 con OpenSSL 3.x |
| Compatibilità certificati | Non supporta certificati legacy (MD5, SHA-1) | Supporta certificati legacy tramite config OS |
| Cross-compilation | Trivial — nessun sysroot C | Complessa — serve cross-compile OpenSSL |
| Audit sicurezza | Memory-safe per costruzione | Dipende dalla versione OpenSSL linkata |
| Feature reqwest | rustls-tls | native-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.txtprima 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.






