use crate::client::error::YahooError;
use reqwest::{cookie::Jar, Client, ClientBuilder};
use std::io::Read;
use std::sync::Arc;
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug)]
pub struct FetchClient {
client: Client,
cookie_jar: Arc<Jar>,
#[allow(dead_code)]
proxy: Option<String>,
auth_proxy: Option<String>,
}
impl FetchClient {
pub fn new(proxy: Option<String>) -> Result<Self, YahooError> {
let auth_proxy = std::env::var("AUTH_PROXY_URL")
.ok()
.or_else(|| proxy.clone());
let cookie_jar = Arc::new(Jar::default());
let mut builder = ClientBuilder::new()
.timeout(DEFAULT_TIMEOUT)
.cookie_store(true)
.cookie_provider(cookie_jar.clone())
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36");
if let Some(proxy_url) = &proxy {
builder =
builder.proxy(reqwest::Proxy::all(proxy_url).map_err(YahooError::NetworkError)?);
}
let client = builder.build().map_err(YahooError::NetworkError)?;
Ok(Self {
client,
cookie_jar,
proxy,
auth_proxy,
})
}
pub fn auth_proxy(&self) -> Option<&String> {
self.auth_proxy.as_ref()
}
pub fn client(&self) -> &Client {
&self.client
}
pub fn cookie_jar(&self) -> &Arc<Jar> {
&self.cookie_jar
}
pub async fn fetch(&self, url: &str) -> Result<String, YahooError> {
self.fetch_with_timeout(url, DEFAULT_TIMEOUT).await
}
pub async fn fetch_json(&self, url: &str) -> Result<String, YahooError> {
self.fetch_json_with_timeout(url, DEFAULT_TIMEOUT).await
}
pub async fn fetch_json_with_timeout(
&self,
url: &str,
timeout: Duration,
) -> Result<String, YahooError> {
let response = match tokio::time::timeout(
timeout,
self.client
.get(url)
.timeout(timeout)
.header("Accept", "application/json")
.header("Accept-Language", "en-US,en;q=0.9")
.header(
"sec-ch-ua",
r#""Chromium";v="122", "Google Chrome";v="122""#,
)
.header("sec-ch-ua-mobile", "?0")
.header("sec-ch-ua-platform", r#""Windows""#)
.send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(YahooError::NetworkError(e)),
Err(_) => {
return Err(YahooError::ParseError(format!(
"Request to {} timed out after {:?}",
url, timeout
)));
}
};
let status = response.status();
if !status.is_success() {
return Err(YahooError::HttpError(
status.as_u16(),
format!(
"HTTP {}: {}",
status,
response.status().canonical_reason().unwrap_or("Unknown")
),
));
}
let content_encoding = response
.headers()
.get("content-encoding")
.and_then(|h| h.to_str().ok())
.unwrap_or("")
.to_lowercase();
let bytes = response.bytes().await.map_err(YahooError::NetworkError)?;
let text = if content_encoding.contains("gzip") || content_encoding.contains("deflate") {
let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);
let mut decompressed = String::new();
decoder.read_to_string(&mut decompressed).map_err(|e| {
YahooError::ParseError(format!("Failed to decompress gzip response: {}", e))
})?;
decompressed
} else if content_encoding.contains("br") {
return Err(YahooError::ParseError(
"Brotli compression detected but not automatically decompressed. This should not happen.".to_string()
));
} else {
match String::from_utf8(bytes.to_vec()) {
Ok(text) => text,
Err(_) => {
let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);
let mut decompressed = String::new();
match decoder.read_to_string(&mut decompressed) {
Ok(_) => decompressed,
Err(_) => {
return Err(YahooError::ParseError(format!(
"Response is not valid UTF-8 and not gzip compressed (length: {} bytes)",
bytes.len()
)));
}
}
}
}
};
Ok(text)
}
pub async fn fetch_with_timeout(
&self,
url: &str,
timeout: Duration,
) -> Result<String, YahooError> {
let response = match tokio::time::timeout(
timeout,
self.client
.get(url)
.timeout(timeout) .header(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
)
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br")
.header(
"sec-ch-ua",
r#""Chromium";v="122", "Google Chrome";v="122""#,
)
.header("sec-ch-ua-mobile", "?0")
.header("sec-ch-ua-platform", r#""Windows""#)
.send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(YahooError::NetworkError(e)),
Err(_) => {
return Err(YahooError::ParseError(format!(
"Request to {} timed out after {:?}",
url, timeout
)));
}
};
let status = response.status();
if !status.is_success() {
return Err(YahooError::HttpError(
status.as_u16(),
format!(
"HTTP {}: {}",
status,
response.status().canonical_reason().unwrap_or("Unknown")
),
));
}
response.text().await.map_err(YahooError::NetworkError)
}
pub async fn fetch_response(&self, url: &str) -> Result<reqwest::Response, YahooError> {
let response = self
.client
.get(url)
.header(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
)
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br")
.header(
"sec-ch-ua",
r#""Chromium";v="122", "Google Chrome";v="122""#,
)
.header("sec-ch-ua-mobile", "?0")
.header("sec-ch-ua-platform", r#""Windows""#)
.send()
.await
.map_err(YahooError::NetworkError)?;
Ok(response)
}
}