Por qué necesitas proxies HTTP en Rust
Si estás construyendo infraestructura de scraping en Rust, tarde o temprano te enfrentarás a bloqueos por IP, rate limiting y CAPTCHAs. Los Rust residential proxies son tu primera línea de defensa: enrutan tus peticiones a través de IPs reales de ISP, haciendo que tu tráfico sea indistinguible del de un usuario normal.
Rust brilla aquí por su modelo de concurrencia sin runtime pesado y su seguridad en tiempo de compilación. Pero configurar proxies correctamente —con autenticación, TLS, rotación y manejo de errores— requiere conocer las entrañas de reqwest, hyper y el ecosistema tokio.
En esta guía cubriremos desde la configuración básica de reqwest proxy hasta una abstracción de pool rotatorio lista para producción, pasando por hyper de bajo nivel, concurrencia con JoinSet, y la decisión entre rustls y native-tls.
Configurar reqwest con proxy, autenticación y TLS personalizado
reqwest es el cliente HTTP de facto en Rust. Soporta proxies HTTP/SOCKS5 de forma nativa cuando activas las feature flags correctas.
Dependencias en Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["proxy", "rustls-tls"], default-features = false }
tokio = { version = "1", features = ["full"] }
Cliente con proxy autenticado y geo-targeting
ProxyHat permite pasar el país y la ciudad directamente en el campo del usuario. Esto es ideal para Rust residential proxies con geo-targeting preciso:
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Geo-targeting: país + ciudad en el username
let proxy_url = "http://user-country-DE-city-berlin: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)")
.timeout(std::time::Duration::from_secs(30))
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?;
let body = resp.text().await?;
println!("IP detectada: {}", body);
Ok(())
}
Sesión sticky con reqwest
Para scraping secuencial donde necesitas la misma IP durante múltiples peticiones, usa el flag session en el username:
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let proxy_url = "http://user-country-US-session-myid123:PASSWORD@gate.proxyhat.com:8080";
let proxy = Proxy::all(proxy_url)?;
let client = reqwest::Client::builder()
.proxy(proxy)
.cookie_store(true)
.build()?;
// Todas las peticiones con este client usan la misma IP de salida
for page in 1..=5 {
let resp = client
.get(format!("https://example.com/page/{}", page))
.send()
.await?;
println!("Página {}: Status {}", page, resp.status());
}
Ok(())
}
hyper de bajo nivel: CONNECT tunnel para HTTPS
Cuando necesitas control total sobre la conexión, hyper te permite manejar el túnel CONNECT manualmente. Esto es útil cuando reqwest te limita —por ejemplo, para implementar connection pooling personalizado o interceptar el handshake CONNECT.
use hyper::client::connect::proxy::HttpConnector;
use hyper::client::Client;
use hyper::Uri;
use hyper_proxy::{Proxy, ProxyConnector, Intercept};
use hyper_rustls::HttpsConnectorBuilder;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Configurar el proxy HTTP
let proxy_uri: Uri = "http://user-country-FR:PASSWORD@gate.proxyhat.com:8080"
.parse()?;
let proxy = Proxy::new(Intercept::All, proxy_uri);
// Construir el connector con rustls para TLS al destino
let https = HttpsConnectorBuilder::new()
.with_native_roots()?
.https_only()
.enable_http2()
.build();
let proxy_connector = ProxyConnector::from_proxy(https, proxy)?;
let client: Client<_, hyper::Body> = Client::builder()
.build(proxy_connector);
let uri: Uri = "https://httpbin.org/ip".parse()?;
let resp = client.get(uri).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(())
}
Nota:
hyper-proxymaneja automáticamente el CONNECT tunnel para tráfico HTTPS a través del proxy HTTP. El proxy ve solo el hostname (no el path ni los headers), mientras que el destino ve tu tráfico TLS normal.
Concurrencia con tokio JoinSet para scraping masivo
El patrón JoinSet de tokio es ideal para scraping concurrente: lanza tareas, aborta las que fallen, y recolecta resultados sin deadlocks.
use reqwest::Proxy;
use std::error::Error;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let urls = vec![
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://httpbin.org/get",
"https://httpbin.org/origin",
];
let mut set: JoinSet<Result<String, reqwest::Error>> = JoinSet::new();
for url in urls {
let url = url.to_string();
set.spawn(async move {
// Cada tarea obtiene su propio client con IP rotatoria
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)
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client.get(&url).send().await?;
resp.text().await
});
}
while let Some(result) = set.join_next().await {
match result {
Ok(Ok(body)) => println!("✓ Éxito ({} bytes)", body.len()),
Ok(Err(e)) => eprintln!("✗ Error de request: {}", e),
Err(e) => eprintln!("✗ Panic en tarea: {}", e),
}
}
Ok(())
}
Limitar concurrencia con Semaphore
Para no saturar el pool de proxies ni hacer trigger de rate limits, envuelve cada tarea en un Semaphore:
use reqwest::Proxy;
use std::error::Error;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let urls: Vec<String> = (1..=50)
.map(|i| format!("https://httpbin.org/get?page={}", i))
.collect();
let semaphore = Arc::new(Semaphore::new(10)); // máx 10 concurrentes
let mut set: JoinSet<Result<String, reqwest::Error>> = JoinSet::new();
for url in urls {
let sem = semaphore.clone();
set.spawn(async move {
let _permit = sem.acquire().await.unwrap();
let proxy_url = "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080";
let proxy = Proxy::all(proxy_url)?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()?;
let resp = client.get(&url).send().await?;
resp.text().await
});
}
let mut ok = 0usize;
let mut err = 0usize;
while let Some(result) = set.join_next().await {
match result {
Ok(Ok(_)) => ok += 1,
_ => err += 1,
}
}
println!("Completadas: {} éxitos, {} errores", ok, err);
Ok(())
}
Rotación de proxies: abstracción con trait
En producción necesitas rotar entre diferentes configuraciones de proxy —distintos países, sesiones, o tipos (residential vs datacenter). Un trait te da flexibilidad para intercambiar estrategias sin cambiar el código de negocio:
use reqwest::{Client, Proxy};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use thiserror::Error;
/// Errores específicos del pool de proxies
#[derive(Debug, Error)]
pub enum ProxyError {
#[error("Todas las proxies fallaron después de {attempts} intentos")]
AllFailed { attempts: usize },
#[error("Error construyendo el cliente: {0}")]
ClientBuild(String),
}
/// Trait para estrategias de rotación de proxies
#[async_trait::async_trait]
pub trait ProxyRotator: Send + Sync {
async fn next_client(&self) -> Result<Client, ProxyError>;
}
/// Rotación round-robin sobre configuraciones de ProxyHat
pub struct RoundRobinRotator {
configs: Vec<String>,
index: AtomicUsize,
}
impl RoundRobinRotator {
pub fn new(countries: &[&str]) -> Self {
let configs: Vec<String> = countries
.iter()
.map(|c| format!("http://user-country-{}:PASSWORD@gate.proxyhat.com:8080", c))
.collect();
Self {
configs,
index: AtomicUsize::new(0),
}
}
}
#[async_trait::async_trait]
impl ProxyRotator for RoundRobinRotator {
async fn next_client(&self) -> Result<Client, ProxyError> {
let idx = self.index.fetch_add(1, Ordering::Relaxed) % self.configs.len();
let proxy_url = &self.configs[idx];
let proxy = Proxy::all(proxy_url.as_str())
.map_err(|e| ProxyError::ClientBuild(e.to_string()))?;
Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()
.map_err(|e| ProxyError::ClientBuild(e.to_string()))
}
}
/// Uso del rotador
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let rotator = Arc::new(RoundRobinRotator::new(&["US", "DE", "FR", "JP"]));
for i in 0..8 {
let client = rotator.next_client().await?;
let resp = client.get("https://httpbin.org/ip").send().await?;
let body = resp.text().await?;
println!("Petición {}: {}", i + 1, body.trim());
}
Ok(())
}
Este patrón te permite intercambiar RoundRobinRotator por un WeightedRotator o un LeastLatencyRotator sin tocar el código de scraping.
Manejo de errores con thiserror y reintentos
Los proxies fallan: timeouts, 403, conexiones reseteadas. Un buen manejo de errores es la diferencia entre un scraper que se cae a las 3am y uno que corre semanas.
use reqwest::{Client, Proxy, StatusCode};
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScrapingError {
#[error("Proxy rechazó la conexión (status {status})")]
ProxyRejected { status: u16 },
#[error("Timeout después de {secs}s")]
Timeout { secs: u64 },
#[error("Error de red: {0}")]
Network(#[from] reqwest::Error),
#[error("Máximos reintentos alcanzados")]
MaxRetries,
}
/// Reintento con backoff exponencial y rotación de proxy en cada intento
pub async fn fetch_with_retry(
url: &str,
proxy_url: &str,
max_retries: u32,
) -> Result<String, ScrapingError> {
let mut attempt = 0u32;
loop {
attempt += 1;
let proxy = Proxy::all(proxy_url).map_err(|e| ScrapingError::Network(
reqwest::Error::new(reqwest::error::Kind::Builder, e)
))?;
let client = Client::builder()
.proxy(proxy)
.timeout(Duration::from_secs(15))
.build()
.map_err(|e| ScrapingError::Network(e))?;
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status();
if status == StatusCode::OK {
return resp.text().await.map_err(ScrapingError::Network);
}
if status.as_u16() == 403 || status.as_u16() == 429 {
eprintln!("Intento {}: status {} — reintentando...", attempt, status);
if attempt >= max_retries {
return Err(ScrapingError::ProxyRejected { status: status.as_u16() });
}
tokio::time::sleep(Duration::from_millis(500 * 2u64.pow(attempt))).await;
continue;
}
return Err(ScrapingError::ProxyRejected { status: status.as_u16() });
}
Err(e) if e.is_timeout() => {
eprintln!("Intento {}: timeout", attempt);
if attempt >= max_retries {
return Err(ScrapingError::Timeout { secs: 15 });
}
tokio::time::sleep(Duration::from_millis(300 * 2u64.pow(attempt))).await;
continue;
}
Err(e) => {
eprintln!("Intento {}: error de red — {}", attempt, e);
if attempt >= max_retries {
return Err(ScrapingError::MaxRetries);
}
tokio::time::sleep(Duration::from_millis(200 * 2u64.pow(attempt))).await;
continue;
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let proxy_url = "http://user-country-ES:PASSWORD@gate.proxyhat.com:8080";
match fetch_with_retry("https://httpbin.org/get", proxy_url, 5).await {
Ok(body) => println!("Éxito: {} bytes", body.len()),
Err(e) => eprintln!("Fallo final: {}", e),
}
Ok(())
}
rustls vs native-tls: qué elegir para proxies
Rust te obliga a elegir tu backend TLS, y esta decisión afecta directamente a cómo se comportan tus conexiones a través de proxies.
| Criterio | rustls | native-tls |
|---|---|---|
| Compilación cruzada | ✅ Fácil (puro Rust) | ❌ Requiere OpenSSL / SChannel |
| Tamaño del binario | Menor | Mayor (vincula C libs) |
| Rendimiento | Comparable, mejor en ARM | Bueno en x86 con AES-NI |
| Compatibilidad PKI | Roots de Mozilla | Roots del SO |
| HTTP/2 | Vía rustls + h2 | Vía native-tls + h2 |
| Certificados corporativos | ❌ Requiere config manual | ✅ Hereda del SO |
| Auditoría de seguridad | ✅ Sin unsafe, auditado | ⚠️ Depende de OpenSSL |
Recomendación: Para scraping con Rust residential proxies, usa rustls-tls. Compila en cualquier target, produce binarios más pequeños, y evita la pesadilla de las versiones de OpenSSL en contenedores Docker.
Configurar rustls en reqwest
// Cargo.toml:
// reqwest = { version = "0.12", default-features = false, features = ["proxy", "rustls-tls", "json"] }
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let proxy = Proxy::all("http://user-country-JP:PASSWORD@gate.proxyhat.com:8080")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.use_rustls_tls() // explícito (ya es default con feature rustls-tls)
.min_tls_version(reqwest::tls::Version::TLS_1_2)
.build()?;
let resp = client.get("https://httpbin.org/ip").send().await?;
println!("{}", resp.text().await?);
Ok(())
}
Feature flags en tiempo de compilación
Para bibliotecas que se usan tanto con como sin proxy, las feature flags de Cargo te permiten compilar solo lo necesario:
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", default-features = false, optional = true }
tokio = { version = "1", features = ["full"] }
thiserror = "1"
async-trait = "0.1"
[features]
default = []
proxy = ["reqwest/proxy", "reqwest/rustls-tls"]
socks5 = ["reqwest/socks"]
native-tls = ["reqwest/native-tls"]
full-proxy = ["proxy", "socks5"]
Luego en tu código Rust, usas #[cfg(feature = "proxy")] para compilar condicionalmente:
use reqwest::Client;
pub struct Scraper {
#[cfg(feature = "proxy")]
client: Client, // construido con proxy
#[cfg(not(feature = "proxy"))]
client: Client, // construido sin proxy
}
impl Scraper {
#[cfg(feature = "proxy")]
pub fn new(proxy_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
let proxy = reqwest::Proxy::all(proxy_url)?;
let client = Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()?;
Ok(Self { client })
}
#[cfg(not(feature = "proxy"))]
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(20))
.build()?;
Ok(Self { client })
}
}
Esto es especialmente útil cuando publicas una crate que otros consumen: no les fuerzas una dependencia de proxy si no la necesitan.
Consideraciones de producción
Logging y observabilidad
Añade tracing desde el inicio. Cada petición proxy debería loguear al menos: URL destino, país del proxy, status code, latencia, y si hubo retry:
use tracing::{info, warn, error};
// En tu función de fetch:
info!(
country = %country,
url = %url,
status = %resp.status(),
latency_ms = elapsed.as_millis(),
"Petición completada"
);
// En errores:
warn!(
country = %country,
url = %url,
attempt = attempt,
error = %e,
"Reintentando"
);
Circuit breaker
Si un país o proxy específico devuelve >50% de errores en una ventana de tiempo, déjalo de lado temporalmente. Puedes implementar un circuit breaker simple con un AtomicU32 para contadores de fallo y un timestamp de última apertura.
Rate limiting ético
Respeta robots.txt y los términos de servicio. Usa delays entre peticiones al mismo dominio. Los Rust residential proxies no te hacen invisible — solo más difícil de bloquear. Un comportamiento irrespetuoso eventualmente resultará en bloqueos de subnet.
Concurrencia vs paralelismo
Con tokio, 1000 tareas concurrentes no significan 1000 hilos. Pero sí significan 1000 conexiones TCP abiertas. Ajusta tu pool de conexiones (pool_max_idle_per_host, pool_idle_timeout) y el tamaño del Semaphore según la capacidad de tu proveedor de proxies.
Key Takeaways
Resumen clave:
- Usa reqwest con feature
proxy+rustls-tlspara el 90% de los casos de Rust HTTP proxy.- Pasa geo-targeting y sesión en el campo
usernamede ProxyHat —no necesitas endpoints diferentes.- Usa hyper solo cuando necesites control de bajo nivel sobre el CONNECT tunnel.
- JoinSet + Semaphore es el patrón canónico para scraping concurrente con rate limiting.
- Abstrae la rotación con un trait para poder intercambiar estrategias sin tocar la lógica de negocio.
- Maneja errores con thiserror y reintentos con backoff exponencial —los proxies fallan constantemente.
- Elige rustls sobre native-tls para contenedores y compilación cruzada.
- Usa feature flags para que tu crate sea usable con y sin proxy.
Si necesitas Rust residential proxies con geo-targeting por país y ciudad, rotación por petición y sesiones sticky, consulta los planes de ProxyHat. Y para más patrones de scraping, visita nuestra guía de use cases y la página de ubicaciones disponibles.






