Pourquoi configurer un proxy HTTP en Rust est un défi
Vous construisez une infrastructure de scraping haute performance en Rust. Le langage offre la vitesse et la sécurité mémoire dont vous rêviez — mais dès qu'il s'agit de router le trafic via un proxy HTTP Rust, la documentation se fait rare. reqwest supporte les proxies, mais comment gérer l'authentification, la rotation d'IP, les sessions sticky, le TLS personnalisé et la concurrence massive ? Et si vous avez besoin de contrôle bas niveau avec hyper ?
Ce guide couvre l'essentiel pour les développeurs Rust qui veulent construire un client HTTP robuste et conscient des proxies — du simple reqwest avec reqwest proxy à un pool de Rust residential proxies rotatifs avec concurrence tokio.
reqwest : configurer un proxy HTTP avec authentification
reqwest est le client HTTP synchrone/asynchrone le plus populaire en Rust. Il supporte nativement les proxies HTTP et SOCKS5 via des feature flags.
Dépendances Cargo
[dependencies]
reqwest = { version = "0.12", features = ["proxy", "rustls-tls"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
Requête basique via proxy avec authentification
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// ProxyHat residential proxy avec geo-targeting US
let proxy_url = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080";
let client = reqwest::Client::builder()
.proxy(Proxy::all(proxy_url)?)
.user_agent("Mozilla/5.0 (compatible; RustScraper/1.0)")
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?;
let body = resp.text().await?;
println!("IP via proxy : {}", body);
Ok(())
}
Astuce ProxyHat : le geo-targeting et les sessions sticky se configurent directement dans le nom d'utilisateur.
user-country-FR-session-abc123verrouille une IP résidentielle française pour toute la session.
TLS personnalisé : rustls vs native-tls
Le choix du backend TLS affecte la compatibilité et les performances. Voici un comparatif :
| Caractéristique | rustls-tls | native-tls |
|---|---|---|
| Backend | ring (pure Rust) | OpenSSL / SChannel |
| Cross-compilation | Simple, pas de C compiler | Nécessite libssl-dev |
| Performance | Excellente (pas de FFI) | Bonne (FFI vers C) |
| Compatibilité | Peut échouer sur certs anciens | Très large (system CA) |
| Feature flag reqwest | rustls-tls | native-tls |
Pour le scraping à grande échelle, rustls est recommandé : compilation statique, pas de dépendance système, et performances prévisibles. Pour la compatibilité maximale avec des certificats d'entreprise, native-tls reste pertinent.
Configuration TLS avancée avec rustls
use reqwest::{Client, Proxy, tls};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let proxy_url = "http://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080";
let client = Client::builder()
.proxy(Proxy::all(proxy_url)?)
.tls_info(true)
.danger_accept_invalid_certs(false)
.min_tls_version(tls::Version::TLS_1_2)
.add_root_certificate(
reqwest::Certificate::from_pem(include_bytes!("custom-ca.pem"))?
)
.build()?;
let resp = client.get("https://example.com").send().await?;
println!("Status : {}", resp.status());
Ok(())
}
hyper bas niveau : CONNECT tunnel pour HTTPS via proxy
Quand reqwest est trop abstrait, hyper offre un contrôle total. Pour le trafic HTTPS via un proxy HTTP, vous devez établir un tunnel CONNECT manuellement.
use hyper::{Request, Body, Client, Uri};
use hyper::client::HttpConnector;
use hyper_proxy::{Proxy, ProxyConnector, Intercept};
use hyper_rustls::HttpsConnector;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let proxy_uri: Uri = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
.parse()?;
let proxy = Proxy::new(Intercept::All, proxy_uri);
let connector = HttpConnector::new();
let tls_connector = HttpsConnector::with_native_roots();
// Le ProxyConnector gère le CONNECT tunnel automatiquement
let proxy_connector = ProxyConnector::from_proxy(tls_connector, proxy)?;
let client: Client<_, Body> = Client::builder()
.build(proxy_connector);
let req = Request::builder()
.uri("https://httpbin.org/ip")
.body(Body::empty())?;
let resp = client.request(req).await?;
println!("Status : {}", resp.status());
let body_bytes = hyper::body::to_bytes(resp.into_body()).await?;
println!("Body : {}", String::from_utf8_lossy(&body_bytes));
Ok(())
}
Dépendances correspondantes :
[dependencies]
hyper = { version = "1", features = ["client", "http1"] }
hyper-proxy = "0.9"
hyper-rustls = "0.27"
tokio = { version = "1", features = ["full"] }
Scraping concurrent avec tokio::task::JoinSet
Le scraping à grande échelle nécessite de la concurrence. JoinSet est idéal : il gère un ensemble de tâches asynchrones avec backpressure intégrée.
use reqwest::{Client, Proxy};
use tokio::task::JoinSet;
use std::sync::Arc;
use std::error::Error;
const URLS: &[&str] = &[
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://httpbin.org/get",
"https://httpbin.org/origin",
];
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = Arc::new(
Client::builder()
.proxy(Proxy::all(
"http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
)?)
.pool_max_idle_per_host(0) // désactive le keep-alive pour forcer la rotation
.build()?
);
let mut tasks = JoinSet::new();
for &url in URLS {
let c = Arc::clone(&client);
tasks.spawn(async move {
match c.get(url).send().await {
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
(url, Ok(format!("{} — {} chars", status, body.len())))
}
Err(e) => (url, Err(e.to_string())),
}
});
}
while let Some(result) = tasks.join_next().await {
match result {
Ok((url, outcome)) => println!("[{}] {:?}", url, outcome),
Err(e) => eprintln!("Task panicked : {}", e),
}
}
Ok(())
}
Avec
pool_max_idle_per_host(0), chaque requête établit une nouvelle connexion — ce qui, combiné aux Rust residential proxies rotatifs de ProxyHat, garantit une IP différente à chaque requête.
Pool de proxies rotatifs : abstraction par trait
Les scrapers sérieux ne se contentent pas d'un seul proxy. Voici une abstraction typée pour un pool de Rust residential proxies avec rotation automatique.
use reqwest::{Client, Proxy, RequestBuilder};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::VecDeque;
/// Trait abstrayant toute source de proxy rotatif
#[async_trait::async_trait]
pub trait ProxyPool: Send + Sync {
async fn next_proxy_url(&self) -> Result<String, ProxyExhausted>;
}
/// Erreur personnalisée quand le pool est épuisé
#[derive(Debug, thiserror::Error)]
#[error("pool de proxies épuisé — aucun proxy disponible")]
pub struct ProxyExhausted;
/// Pool round-robin de proxies ProxyHat avec geo-ciblage
pub struct ProxyHatPool {
base_user: String,
password: String,
countries: Vec<String>,
index: RwLock<usize>,
}
impl ProxyHatPool {
pub fn new(base_user: &str, password: &str, countries: Vec<String>) -> Self {
Self {
base_user: base_user.to_string(),
password: password.to_string(),
countries,
index: RwLock::new(0),
}
}
}
#[async_trait::async_trait]
impl ProxyPool for ProxyHatPool {
async fn next_proxy_url(&self) -> Result<String, ProxyExhausted> {
if self.countries.is_empty() {
return Err(ProxyExhausted);
}
let mut idx = self.index.write().await;
let country = &self.countries[*idx % self.countries.len()];
*idx += 1;
// Format ProxyHat : user-country-XX:password@host:port
let username = format!("{}-country-{}", self.base_user, country);
Ok(format!(
"http://{}:{}@gate.proxyhat.com:8080",
username, self.password
))
}
}
/// Client HTTP qui injecte automatiquement un proxy rotatif
pub struct RotatingClient {
pool: Arc<dyn ProxyPool>,
client_builder: reqwest::ClientBuilder,
}
impl RotatingClient {
pub fn new(pool: Arc<dyn ProxyPool>) -> Self {
Self {
pool,
client_builder: Client::builder()
.pool_max_idle_per_host(0)
.timeout(std::time::Duration::from_secs(30)),
}
}
/// Construit un client avec le prochain proxy du pool
pub async fn client(&self) -> Result<Client, Box<dyn std::error::Error>> {
let proxy_url = self.pool.next_proxy_url().await?;
Ok(self.client_builder
.clone()
.proxy(Proxy::all(&proxy_url)?)
.build()?)
}
}
// --- Utilisation ---
// let pool = Arc::new(ProxyHatPool::new(
// "user", "PASSWORD", vec!["US".into(), "DE".into(), "FR".into()]
// ));
// let rotating = RotatingClient::new(pool);
// let client = rotating.client().await?;
// let resp = client.get("https://httpbin.org/ip").send().await?;
Ce pattern sépare la logique de sélection du proxy de la logique HTTP — vous pouvez remplacer ProxyHatPool par n'importe quelle implémentation (fichier, API distante, base de données).
Gestion d'erreurs robuste avec thiserror
Un scraper de production doit distinguer les erreurs réseau, les erreurs de proxy, les erreurs HTTP et les erreurs de parsing. thiserror rend cela propre.
use thiserror::Error;
use reqwest::StatusCode;
#[derive(Debug, Error)]
pub enum ScraperError {
#[error("erreur réseau : {0}")]
Network(#[from] reqwest::Error),
#[error("proxy indisponible : {0}")]
ProxyUnavailable(String),
#[error("HTTP {status} pour {url} — corps : {body_preview}")]
Http {
status: StatusCode,
url: String,
body_preview: String,
},
#[error("CAPTCHA détecté sur {url}")]
Captcha { url: String },
#[error("timeout après {elapsed_ms}ms pour {url}")]
Timeout {
elapsed_ms: u64,
url: String,
},
#[error("erreur de parsing : {0}")]
Parse(#[from] serde_json::Error),
}
/// Exécute une requête avec retry et classification d'erreur
pub async fn fetch_with_retry(
client: &reqwest::Client,
url: &str,
max_retries: usize,
) -> Result<String, ScraperError> {
let mut attempt = 0;
loop {
let start = std::time::Instant::now();
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status();
let body = resp.text().await?;
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
attempt += 1;
if attempt >= max_retries {
return Err(ScraperError::Http {
status,
url: url.to_string(),
body_preview: body.chars().take(100).collect(),
});
}
tokio::time::sleep(
std::time::Duration::from_millis(500 * attempt as u64)
).await;
continue;
}
if body.contains("captcha") || body.contains("cf-challenge") {
return Err(ScraperError::Captcha {
url: url.to_string(),
});
}
if !status.is_success() {
return Err(ScraperError::Http {
status,
url: url.to_string(),
body_preview: body.chars().take(100).collect(),
});
}
return Ok(body);
}
Err(e) => {
if e.is_timeout() {
attempt += 1;
if attempt >= max_retries {
return Err(ScraperError::Timeout {
elapsed_ms: start.elapsed().as_millis() as u64,
url: url.to_string(),
});
}
continue;
}
return Err(ScraperError::Network(e));
}
}
}
}
Feature flags : compilation conditionnelle pour le support proxy
Toutes les applications Rust n'ont pas besoin de proxy. Les feature flags permettent de compiler le support proxy uniquement quand c'est nécessaire, réduisant le binaire et le temps de compilation.
Configuration Cargo.toml
[features]
default = ["rustls-tls"]
proxy-support = ["reqwest/proxy"]
socks5-support = ["reqwest/socks"]
rustls-tls = ["reqwest/rustls-tls"]
native-tls = ["reqwest/native-tls"]
[dependencies]
reqwest = { version = "0.12", default-features = false }
tokio = { version = "1", features = ["full"] }
Code conditionnel
pub fn build_client(proxy_url: Option<&str>) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
let mut builder = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.user_agent("RustScraper/1.0");
#[cfg(feature = "proxy-support")]
if let Some(url) = proxy_url {
builder = builder.proxy(reqwest::Proxy::all(url)?);
}
#[cfg(not(feature = "proxy-support"))]
if proxy_url.is_some() {
eprintln!("Attention : proxy-support désactivé à la compilation — proxy ignoré");
}
Ok(builder.build()?)
}
// Compilation : cargo build --features proxy-support,socks5-support
// Binaire minimal : cargo build (pas de proxy, pas de SOCKS5)
Ce pattern est particulièrement utile pour les binaires CLI où la taille finale compte, ou pour les environnements embarqués.
Sessions sticky vs rotation par requête
Avec ProxyHat, le comportement de rotation dépend du flag dans le nom d'utilisateur :
| Mode | Format username | Cas d'usage |
|---|---|---|
| Rotation par requête | user-country-US | Scraping SERP, monitoring de prix |
| Session sticky | user-country-US-session-abc123 | Login, navigation multi-page, panier |
| Geo-ciblage ville | user-country-US-city-newyork | Contenu localisé, tests régionaux |
Pour les sessions sticky, gardez le même session-xxx tant que vous avez besoin de la même IP. Le TTL typique est de 10 à 30 minutes selon le fournisseur.
Bonnes pratiques de production
- Connexion pooling : désactivez-le (
pool_max_idle_per_host(0)) si vous voulez une IP différente à chaque requête avec des proxies résidentiels rotatifs. - Timeouts : configurez
connect_timeoutettimeoutséparément. Les proxies résidentiels sont plus lents que les datacenter — prévoyez 10-30s. - Retry avec backoff exponentiel : les échecs transitoires sont normaux avec les proxies résidentiels. Réessayez 2-3 fois avec un délai croissant.
- Rate limiting par domaine : même avec des proxies, trop de requêtes simultanées vers un même domaine déclenche des CAPTCHAs.
- Logging structuré : journalisez l'IP proxy, le pays, le code HTTP et le temps de réponse pour chaque requête.
- Respect de robots.txt : vérifiez toujours le
robots.txtavant de scraper un domaine, et respectez les directivesCrawl-delay.
Points clés à retenir
- reqwest couvre 90% des cas — proxy, auth, TLS en quelques lignes.
- hyper est nécessaire pour le contrôle bas niveau du tunnel CONNECT.
- JoinSet offre une concurrence propre avec backpressure intégrée.
- Abstrairez le pool de proxies via un trait pour séparer la logique de sélection de la logique HTTP.
- thiserror permet de classifier les erreurs (réseau, proxy, CAPTCHA, parsing) proprement.
- rustls pour la portabilité, native-tls pour la compatibilité maximale.
- Les feature flags réduisent le binaire en excluant le support proxy quand il n'est pas nécessaire.
- Les proxies résidentiels rotatifs de ProxyHat se configurent entièrement dans le nom d'utilisateur.
Prochaines étapes
Prêt à construire votre infrastructure de scraping Rust avec des proxies résidentiels fiables ? Visitez ProxyHat — Tarification pour découvrir les plans adaptés à votre volume, ou explorez nos localisations de proxies pour voir la couverture géographique disponible. Pour des patterns de scraping avancés, consultez notre guide de web scraping en Rust.






