Dlaczego Rust i proxy HTTP to naturalne połączenie
Rust staje się językiem wyboru dla wysokowydajnej infrastruktury scrapingowej. Bez garbage collectora, z przewidywalnym zarządzaniem pamięcią i natywnym async/await na bazie tokio — oferuje przepustowość, jakiej Python czy Node.js nie potrafią dorównać przy tysiącach jednoczesnych połączeń. Problem? Większość tutoriali pomija warstwę proxy, a bez niej każdy poważny scraper trafia na rate limity, CAPTCHAl i blokady geo.
W tym artykule pokazuję, jak skonfigurować Rust HTTP proxy na każdym poziomie abstrakcji — od wysokopoziomowego reqwest, przez niskopoziomowy hyper, aż po własną abstrakcję puli rotating proxy. Używam Rust residential proxies od ProxyHat, ale wzorce są uniwersalne.
reqwest proxy — szybki start z autoryzacją i TLS
reqwest to de facto standardowy klient HTTP w Ruście. Obsługuje proxy od razu — wystarczy przekazać URL proxy w builderze.
Podstawowa konfiguracja z autoryzacją
use reqwest::Proxy;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Proxy z autoryzacją — format ProxyHat
let proxy_url = "http://myuser-country-US:mypass@gate.proxyhat.com:8080";
let client = reqwest::Client::builder()
.proxy(Proxy::all(proxy_url)?)
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?;
println!("Status: {}", resp.status());
println!("IP: {}", resp.text().await?);
Ok(())
}
Kluczowe parametry ProxyHat w nazwie użytkownika:
country-US— geo-targeting na poziomie krajucountry-DE-city-berlin— targeting miastosession-abc123— sticky session (ten sam IP przez sesję)
Custom TLS z rustls
W Cargo.toml wybierasz backend TLS feature flagą:
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls", # lub "native-tls"
"proxy", # włącza obsługę proxy
"json",
] }
tokio = { version = "1", features = ["full"] }
rustls daje statyczne linkowanie i brak zależności od OpenSSL — idealne dla kontenerów Alpine i cross-compilation. native-tls używa systemowego TLS (Schannel na Windows, Security.framework na macOS, OpenSSL na Linux). Wybierz rustls, jeśli chcesz powtarzalne buildy; native-tls, jeśli potrzebujesz systemowych certyfikatów enterprise.
hyper z proxy-connect — kontrola na niskim poziomie
Gdy reqwest to za mało — np. potrzebujesz dostępu do warstwy połączenia, custom connection poolingu albo mierzenia czasu DNS — schodzisz do hyper. Proxy HTTPS przez HTTP proxy wymaga metody CONNECT, którą hyper obsługuje przez hyper-proxy.
use hyper::{Client, Uri, Request, Body};
use hyper_proxy::{Proxy, ProxyConnector, Intercept};
use hyper_rustls::HttpsConnectorBuilder;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// TLS connector z rustls
let https = HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_or_http()
.enable_http2()
.build();
// Konfiguracja proxy
let proxy_url: Uri = "http://myuser-country-DE:mypass@gate.proxyhat.com:8080"
.parse()?;
let proxy = Proxy::new(Intercept::All, proxy_url);
let proxy_connector = ProxyConnector::from_proxy(https, proxy)?;
let client = Client::builder()
.build(proxy_connector);
let uri: Uri = "https://httpbin.org/headers".parse()?;
let resp = client.get(uri).await?;
println!("Status: {}", resp.status());
let body = hyper::body::to_bytes(resp.into_body()).await?;
println!("Body: {}", String::from_utf8_lossy(&body));
Ok(())
}
# Cargo.toml — zależności dla hyper
[dependencies]
hyper = { version = "1", features = ["client", "http1", "http2"] }
hyper-proxy = "0.9"
hyper-rustls = "0.27"
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
Zalety hyper: pełna kontrola nad CONNECT tunelem, możliwość wstrzykiwania custom resolvera DNS, i dostęp do metryk połączenia. Wady — więcej boilerplate, ręczne zarządzanie body streamem.
Współbieżny scraping z tokio::task::JoinSet
Pojedyncze żądanie przez proxy jest wolne (latency proxy + TLS handshake). Prawdziwa wydajność przychodzi z współbieżnością. JoinSet z tokio pozwala zarządzać pulą zadań z limitem jednoczesnych połączeń.
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 proxy_url = "http://myuser-country-US:mypass@gate.proxyhat.com:8080";
let client = Arc::new(
reqwest::Client::builder()
.proxy(Proxy::all(proxy_url)?)
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.pool_max_idle_per_host(0) // wyłącz keep-alive pool — rotacja IP
.build()?
);
let targets = vec![
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://httpbin.org/get",
"https://httpbin.org/origin",
];
let mut tasks = JoinSet::new();
let max_concurrent = 3;
for url in &targets {
// Czekaj, aż liczba aktywnych zadań spadnie poniżej limitu
while tasks.len() >= max_concurrent {
let result = tasks.join_next().await.unwrap()?;
println!("Zakończone: {}", result?);
}
let c = client.clone();
let u = url.to_string();
tasks.spawn(async move {
let resp = c.get(&u).send().await?;
let status = resp.status().as_u16();
resp.text().await?;
Ok::<u16, reqwest::Error>(status)
});
}
// Zbierz pozostałe wyniki
while let Some(result) = tasks.join_next().await {
let status = result??;
println!("Zakończone: {}", status);
}
Ok(())
}
Ważny detal: pool_max_idle_per_host(0) wyłącza keep-alive pooling w reqwest. Gdy używasz Rust residential proxies z rotacją per-request, keep-alive może przywracać stare IP — wyłączenie poolingu wymusza nowe połączenie za każdym razem.
Rotating proxy pool — abstrakcja przez trait
Hardcodowanie URL proxy w kodzie nie skaluje. Zamiast tego zdefiniuj trait, który dostarcza następny proxy z puli — z rotacją round-robin, sticky sessions albo weighted random.
use reqwest::Proxy;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
/// Trait abstrakcji dla puli proxy
pub trait ProxyPool: Send + Sync {
fn next_proxy(&self) -> Result<Proxy, ProxyPoolError>;
}
#[derive(Debug, thiserror::Error)]
pub enum ProxyPoolError {
#[error("Pula proxy jest pusta")]
Empty,
#[error("Błąd parsowania URL proxy: {0}")]
InvalidUrl(#[from] reqwest::UrlError),
}
/// Pula z rotacją round-robin + geo-targeting
pub struct RotatingProxyPool {
base_user: String,
password: String,
gateway: String,
countries: Vec<String>,
index: AtomicUsize,
}
impl RotatingProxyPool {
pub fn new(
base_user: &str,
password: &str,
gateway: &str,
countries: Vec<String>,
) -> Self {
Self {
base_user: base_user.to_string(),
password: password.to_string(),
gateway: gateway.to_string(),
countries,
index: AtomicUsize::new(0),
}
}
}
impl ProxyPool for RotatingProxyPool {
fn next_proxy(&self) -> Result<Proxy, ProxyPoolError> {
if self.countries.is_empty() {
return Err(ProxyPoolError::Empty);
}
let idx = self.index.fetch_add(1, Ordering::Relaxed);
let country = &self.countries[idx % self.countries.len()];
// Format: user-country-US:pass@gate.proxyhat.com:8080
let proxy_url = format!(
"http://{}-country-{}:{}@{}",
self.base_user, country, self.password, self.gateway
);
Proxy::all(&proxy_url).map_err(ProxyPoolError::InvalidUrl)
}
}
/// Sticky session — ten sam IP przez całą sesję
pub struct StickyProxyPool {
proxy: Proxy,
}
impl StickyProxyPool {
pub fn new(
user: &str,
password: &str,
gateway: &str,
session_id: &str,
) -> Result<Self, ProxyPoolError> {
let url = format!(
"http://{}-session-{}:{}@{}",
user, session_id, password, gateway
);
Ok(Self {
proxy: Proxy::all(&url).map_err(ProxyPoolError::InvalidUrl)?,
})
}
}
impl ProxyPool for StickyProxyPool {
fn next_proxy(&self) -> Result<Proxy, ProxyPoolError> {
// Zwraca ten sam proxy za każdym razem
Proxy::all(self.proxy.raw_proxy_url()
.ok_or(ProxyPoolError::Empty)?)
.map_err(ProxyPoolError::InvalidUrl)
}
}
// Użycie:
// let pool = RotatingProxyPool::new(
// "myuser", "mypass", "gate.proxyhat.com:8080",
// vec!["US".into(), "DE".into(), "JP".into()],
// );
// let proxy = pool.next_proxy()?;
Ta abstrakcja pozwala zamieniać strategie rotacji bez zmiany kodu scrapera. W testach jednostkowych możesz podpiąć mock implementację; w produkcji — RotatingProxyPool albo StickyProxyPool.
Error handling z thiserror — typowe błędy proxy
Proxy dodaje warstwę błędów: timeout połączenia, 407 Proxy Authentication Required, TLS handshake failure, i rate limiting upstream. Zdefiniuj typ błędu z thiserror, żeby uniknąć Box w produkcji.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScraperError {
#[error("Błąd proxy: {0}")]
Proxy(#[from] reqwest::Error),
#[error("Błąd puli proxy: {0}")]
Pool(#[from] ProxyPoolError),
#[error("Rate limit — retry po {wait_ms}ms")]
RateLimited { wait_ms: u64 },
#[error("CAPTCHA wykryta na {url}")]
Captcha { url: String },
#[error("Timeout po {elapsed_ms}ms dla {url}")]
Timeout { url: String, elapsed_ms: u64 },
}
/// Retry z exponential backoff
pub async fn fetch_with_retry(
client: &reqwest::Client,
url: &str,
max_retries: u32,
) -> Result<String, ScraperError> {
let mut attempt = 0;
loop {
attempt += 1;
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status();
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
let wait = 500 * 2u64.pow(attempt);
if attempt > max_retries {
return Err(ScraperError::RateLimited { wait_ms: wait });
}
tokio::time::sleep(
std::time::Duration::from_millis(wait)
).await;
continue;
}
return resp.text().await.map_err(ScraperError::Proxy);
}
Err(e) => {
if e.is_timeout() {
if attempt > max_retries {
return Err(ScraperError::Timeout {
url: url.to_string(),
elapsed_ms: 30_000,
});
}
tokio::time::sleep(
std::time::Duration::from_millis(1000 * attempt as u64)
).await;
continue;
}
return Err(ScraperError::Proxy(e));
}
}
}
}
rustls vs native-tls — porównanie dla proxy
Wybór backendu TLS wpływa na kompatybilność, rozmiar binarki i zachowanie przy proxy. Oto porównanie:
| Cecha | rustls | native-tls |
|---|---|---|
| Linkowanie | Statyczne (pure Rust) | Dynamiczne (systemowe lib) |
| Zależności C | Brak | OpenSSL (Linux) |
| Rozmiar binarki | ~2 MB mniejszy | Większy |
| Certyfikaty enterprise | Tylko WebPKI roots | System store |
| Cross-compilation | Prosta | Wymaga cross-compile OpenSSL |
| HTTP/2 | Tak (via ALPN) | Zależy od platformy |
| Dojrzałość | Produkcyjna od 2023 | Produkcyjna od lat |
Rekomendacja: Dla scraping infrastruktury w kontenerach — rustls-tls. Dla środowisk korporacyjnych z własnymi CA — native-tls. Przełączanie to jedna linijka w Cargo.toml.
Feature flags — kompilacja warunkowa dla proxy
Nie każdy build potrzebuje proxy (np. testy jednostkowe, CLI bez proxy). Użyj feature flags, żeby warunkowo włączyć obsługę proxy.
# Cargo.toml
[features]
default = ["proxy-support"]
proxy-support = ["reqwest/proxy"]
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"json",
] }
tokio = { version = "1", features = ["full"] }
# W kodzie:
// src/client.rs
pub fn build_client(use_proxy: bool, 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));
#[cfg(feature = "proxy-support")]
if let Some(url) = proxy_url {
builder = builder.proxy(reqwest::Proxy::all(url)?);
}
Ok(builder.build()?)
}
// Build bez proxy:
// cargo build --no-default-features
// Build z proxy:
// cargo build
Dzięki temu binarka bez proxy jest mniejsza i szybciej się kompiluje. W CI możesz testować obie ścieżki: cargo test --no-default-features i cargo test.
Produkcyjny scraper — pełny przykład z retry i circuit breaker
Łącząc wszystko — trait proxy pool, error handling, współbieżność i TLS — budujemy scraper gotowy na produkcję:
use reqwest::{Client, Proxy, StatusCode};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use tokio::task::JoinSet;
pub struct ProductionScraper {
pool: Arc<dyn ProxyPool>,
max_retries: u32,
max_concurrent: usize,
consecutive_failures: AtomicU32,
circuit_threshold: u32,
}
impl ProductionScraper {
pub fn new(
pool: Arc<dyn ProxyPool>,
max_retries: u32,
max_concurrent: usize,
circuit_threshold: u32,
) -> Self {
Self {
pool,
max_retries,
max_concurrent,
consecutive_failures: AtomicU32::new(0),
circuit_threshold,
}
}
fn build_client(&self) -> Result<Client, ScraperError> {
let proxy = self.pool.next_proxy()?;
Client::builder()
.proxy(proxy)
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.pool_max_idle_per_host(0)
.build()
.map_err(ScraperError::Proxy)
}
/// Sprawdź circuit breaker — jeśli za dużo błędów, odczekaj
async fn check_circuit(&self) -> Result<(), ScraperError> {
let failures = self.consecutive_failures.load(Ordering::Relaxed);
if failures >= self.circuit_threshold {
let wait = Duration::from_secs(30 * 2u64.pow(failures / self.circuit_threshold));
eprintln!(
"Circuit breaker otwarty — {} błędów, czekam {:?}",
failures, wait
);
tokio::time::sleep(wait).await;
}
Ok(())
}
pub async fn fetch_urls(&self, urls: &[&str]) -> Vec<Result<String, ScraperError>> {
let mut results = Vec::with_capacity(urls.len());
let mut tasks = JoinSet::new();
for url in urls {
while tasks.len() >= self.max_concurrent {
self.check_circuit().await.ok();
let result = tasks.join_next().await.unwrap();
match &result {
Ok(Ok(_)) => {
self.consecutive_failures.store(0, Ordering::Relaxed);
}
_ => {
self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
}
}
results.push(result.map_err(|e| ScraperError::Proxy(
e.into()
)).and_then(|r| r));
}
let client = self.build_client()?;
let url = url.to_string();
let max_retries = self.max_retries;
tasks.spawn(async move {
let mut attempt = 0u32;
loop {
attempt += 1;
match client.get(&url).send().await {
Ok(resp) => {
match resp.status() {
s if s.is_success() => {
break Ok(resp.text().await
.map_err(ScraperError::Proxy))
}
StatusCode::TOO_MANY_REQUESTS => {
if attempt > max_retries {
break Err(ScraperError::RateLimited {
wait_ms: 500 * 2u64.pow(attempt),
});
}
tokio::time::sleep(
Duration::from_millis(500 * 2u64.pow(attempt))
).await;
continue;
}
StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => {
break Err(ScraperError::Captcha {
url: url.clone(),
});
}
_ => {
if attempt > max_retries {
break Err(ScraperError::Proxy(
reqwest::Error::from(
resp.error_for_status().unwrap_err()
)
));
}
tokio::time::sleep(
Duration::from_millis(300 * attempt as u64)
).await;
continue;
}
}
}
Err(e) if e.is_timeout() => {
if attempt > max_retries {
break Err(ScraperError::Timeout {
url: url.clone(),
elapsed_ms: 30_000,
});
}
tokio::time::sleep(
Duration::from_millis(1000 * attempt as u64)
).await;
continue;
}
Err(e) => break Err(ScraperError::Proxy(e)),
}
}
});
}
// Zbierz pozostałe wyniki
while let Some(result) = tasks.join_next().await {
match &result {
Ok(Ok(_)) => {
self.consecutive_failures.store(0, Ordering::Relaxed);
}
_ => {
self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
}
}
results.push(
result
.map_err(|e| ScraperError::Proxy(e.into()))
.and_then(|r| r)
);
}
results
}
}
Residential vs datacenter vs mobile proxies — co wybrać w Ruście
Wybór typu proxy zależy od przypadku użycia:
- Residential proxies — IP z prawdziwych ISP. Najlepsze do SERP scraping, e-commerce i stron z agresywną anti-bot ochroną. Wyższa latencja, ale niższy wskaźnik blokad.
- Datacenter proxies — szybkie, tanie, ale łatwe do wykrycia. Dobre do API bez anti-bota, bulk data transfer, i wstępnego crawlowania.
- Mobile proxies — IP z sieci komórkowych. Najwyższa zaufaność, idealne do social media i ticketing. Najdroższe i najwolniejsze.
W Ruście możesz łączyć typy — datacenter do wstępnego filtrowania, residential do precyzyjnego pobierania. Trait ProxyPool ułatwia tę strategię.
Etyka i legalność
Scraping przez proxy nie zwalnia z odpowiedzialności:
- Szanuj
robots.txt— przynajmniej dla sygnałów o rate limitach. - Przestrzegaj GDPR/CCPA — nie scrapuj danych osobowych bez podstawy prawnej.
- Czytaj ToS serwisów — niektóre wyraźnie zabraniają automatycznego dostępu.
- Używaj rozsądnych rate limitów — nawet przez proxy, zalewanie serwera to abuse.
Kluczowe wnioski
- reqwest z feature
proxyto najszybsza droga do działającego proxy w Ruście.- hyper daje pełną kontrolę nad CONNECT tunelem — używaj, gdy reqwest to za mało.
- JoinSet + limit jednoczesnych zadań = kontrolowana współbieżność bez zalewania proxy.
- Trait ProxyPool pozwala zamieniać strategie rotacji bez zmiany kodu scrapera.
- thiserror + typed errors = czysta obsługa błędów proxy w produkcji.
- rustls dla kontenerów, native-tls dla enterprise — przełączanie jedną flagą.
- Feature flags pozwalają budować bez proxy — szybsza kompilacja i mniejsza binarka.
- Circuit breaker chroni przed kaskadowymi błędami przy awarii proxy upstream.
Gotowy na budowę? Sprawdź plany ProxyHat i zacznij od residential proxy z geo-targetingiem — albo przejrzyj dostępne lokalizacje, żeby wybrać kraje dla swojej puli. Więcej wzorców scrapingowych znajdziesz w naszym przewodniku po web scrapingu.






