Guide complet du proxy HTTP en Rust : reqwest, hyper et scraping concurrent

Apprenez à configurer des proxies HTTP en Rust avec reqwest, hyper, tokio JoinSet, un pool de proxies tournant, thiserror, rustls vs native-tls et les feature flags — avec 6+ blocs de code exécutables.

Guide complet du proxy HTTP en Rust : reqwest, hyper et scraping concurrent

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-abc123 verrouille 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éristiquerustls-tlsnative-tls
Backendring (pure Rust)OpenSSL / SChannel
Cross-compilationSimple, pas de C compilerNécessite libssl-dev
PerformanceExcellente (pas de FFI)Bonne (FFI vers C)
CompatibilitéPeut échouer sur certs anciensTrès large (system CA)
Feature flag reqwestrustls-tlsnative-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 :

ModeFormat usernameCas d'usage
Rotation par requêteuser-country-USScraping SERP, monitoring de prix
Session stickyuser-country-US-session-abc123Login, navigation multi-page, panier
Geo-ciblage villeuser-country-US-city-newyorkContenu 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_timeout et timeout sé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.txt avant de scraper un domaine, et respectez les directives Crawl-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.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog