use crate::fetchers::config::FetcherConfig;
use crate::fetchers::proxy::ProxyRotator;
use crate::fetchers::response::Response;
use std::collections::HashMap;
use std::time::Duration;
pub struct Fetcher {
config: FetcherConfig,
clients: Vec<reqwest::Client>,
rotator: Option<ProxyRotator>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FetcherError {
#[error("request failed after {attempts} attempt(s): {source}")]
ExhaustedRetries {
attempts: u32,
#[source]
source: reqwest::Error,
},
#[error("response body{} exceeds the {limit_bytes}-byte cap", advertised_bytes.map(|b| format!(" (advertised {b} bytes)")).unwrap_or_default())]
TooLarge {
limit_bytes: usize,
advertised_bytes: Option<u64>,
},
#[error("body read failed: {0}")]
BodyRead(#[source] reqwest::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
}
impl FetcherError {
#[must_use]
pub fn transport_error(&self) -> Option<&reqwest::Error> {
match self {
FetcherError::ExhaustedRetries { source, .. } => Some(source),
FetcherError::BodyRead(e) => Some(e),
_ => None,
}
}
#[must_use]
pub fn is_timeout(&self) -> bool {
self.transport_error()
.is_some_and(reqwest::Error::is_timeout)
}
#[must_use]
pub fn is_connect(&self) -> bool {
self.transport_error()
.is_some_and(reqwest::Error::is_connect)
}
}
impl Fetcher {
pub fn new(config: FetcherConfig) -> Result<Self, FetcherError> {
if !config.verify_ssl {
log::warn!(
"SSL certificate verification is DISABLED. This is insecure \
and must not be used in production."
);
}
let (clients, rotator) = if config.proxy_list.is_empty() {
(vec![Self::build_client(&config, None)?], None)
} else {
let clients = config
.proxy_list
.iter()
.map(|p| Self::build_client(&config, Some(p)))
.collect::<Result<Vec<_>, _>>()?;
let rotator = ProxyRotator::new(config.proxy_list.clone());
(clients, rotator)
};
Ok(Self {
config,
clients,
rotator,
})
}
fn build_client(
config: &FetcherConfig,
proxy_override: Option<&str>,
) -> Result<reqwest::Client, FetcherError> {
let mut builder = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.danger_accept_invalid_certs(!config.verify_ssl);
if config.follow_redirects {
builder = builder.redirect(reqwest::redirect::Policy::limited(
config.max_redirects as usize,
));
} else {
builder = builder.redirect(reqwest::redirect::Policy::none());
}
if let Some(proxy_url) = proxy_override {
if let Ok(proxy) = reqwest::Proxy::all(proxy_url) {
builder = builder.proxy(proxy);
}
} else {
if let Some(proxy_url) = config.proxies.get("http") {
if let Ok(proxy) = reqwest::Proxy::http(proxy_url) {
builder = builder.proxy(proxy);
}
}
if let Some(proxy_url) = config.proxies.get("https") {
if let Ok(proxy) = reqwest::Proxy::https(proxy_url) {
builder = builder.proxy(proxy);
}
}
let mut wildcard_keys: Vec<&String> = config
.proxies
.keys()
.filter(|k| k.as_str() != "http" && k.as_str() != "https")
.collect();
wildcard_keys.sort();
for key in wildcard_keys {
if let Ok(proxy) = reqwest::Proxy::all(&config.proxies[key]) {
builder = builder.proxy(proxy);
}
}
if let Some(ref proxy_url) = config.proxy {
if let Ok(proxy) = reqwest::Proxy::all(proxy_url) {
builder = builder.proxy(proxy);
}
}
}
Ok(builder.build()?)
}
fn next_client(&self) -> &reqwest::Client {
match &self.rotator {
Some(rotator) => &self.clients[rotator.next_index()],
None => &self.clients[0],
}
}
pub async fn get(&self, url: &str) -> Result<Response, FetcherError> {
self.request(reqwest::Method::GET, url, None, None, None)
.await
}
pub async fn post(
&self,
url: &str,
body: Option<&str>,
json: Option<&serde_json::Value>,
) -> Result<Response, FetcherError> {
self.request(reqwest::Method::POST, url, body, json, None)
.await
}
pub async fn put(
&self,
url: &str,
body: Option<&str>,
json: Option<&serde_json::Value>,
) -> Result<Response, FetcherError> {
self.request(reqwest::Method::PUT, url, body, json, None)
.await
}
pub async fn delete(&self, url: &str) -> Result<Response, FetcherError> {
self.request(reqwest::Method::DELETE, url, None, None, None)
.await
}
pub(crate) async fn request(
&self,
method: reqwest::Method,
url: &str,
body: Option<&str>,
json: Option<&serde_json::Value>,
extra_headers: Option<&HashMap<String, String>>,
) -> Result<Response, FetcherError> {
let mut headers = self.config.build_headers(url, self.config.stealthy_headers);
if let Some(extra) = extra_headers {
for (k, v) in extra {
headers.insert(k.clone(), v.clone());
}
}
let mut last_error: Option<reqwest::Error> = None;
for attempt in 0..=self.config.retries {
let attempt_started = std::time::Instant::now();
let mut req = self.next_client().request(method.clone(), url);
for (key, value) in &headers {
req = req.header(key.as_str(), value.as_str());
}
if let Some(json_val) = json {
req = req.json(json_val);
} else if let Some(text_body) = body {
req = req.body(text_body.to_string());
}
match req.send().await {
Ok(mut resp) => {
let status_code = resp.status().as_u16();
let final_url = resp.url().to_string();
let mut resp_headers: HashMap<String, String> = HashMap::new();
for (key, value) in resp.headers() {
if let Ok(v) = value.to_str() {
resp_headers.insert(key.to_string(), v.to_string());
}
}
let content_type = resp_headers
.get("content-type")
.cloned()
.unwrap_or_default();
let max_body = self.config.max_body_bytes;
if let Some(len) = resp.content_length() {
let too_large = match usize::try_from(len) {
Ok(n) => n > max_body,
Err(_) => true,
};
if too_large {
return Err(FetcherError::TooLarge {
limit_bytes: max_body,
advertised_bytes: Some(len),
});
}
}
let mut bytes: Vec<u8> = Vec::new();
loop {
match resp.chunk().await {
Ok(Some(chunk)) => {
let new_len = bytes.len().checked_add(chunk.len());
if new_len.map(|n| n > max_body).unwrap_or(true) {
return Err(FetcherError::TooLarge {
limit_bytes: max_body,
advertised_bytes: None,
});
}
bytes.extend_from_slice(&chunk);
}
Ok(None) => break,
Err(e) => {
return Err(FetcherError::BodyRead(e));
}
}
}
let body_text = crate::fetchers::encoding::decode_body_capped(
&bytes,
&content_type,
max_body,
);
let mut response = Response::new(
status_code,
content_type,
body_text,
final_url,
resp_headers,
);
response.set_latency(attempt_started.elapsed());
return Ok(response);
}
Err(e) => {
last_error = Some(e);
if attempt < self.config.retries {
tokio::time::sleep(Duration::from_secs(self.config.retry_delay_secs)).await;
}
}
}
}
Err(FetcherError::ExhaustedRetries {
attempts: self.config.retries.saturating_add(1),
source: last_error.expect("loop ran at least once, so a send error was recorded"),
})
}
}