diff --git a/impit/src/errors.rs b/impit/src/errors.rs index 1a6ae737..8bcbf959 100644 --- a/impit/src/errors.rs +++ b/impit/src/errors.rs @@ -105,6 +105,10 @@ impl From for ImpitError { } impl ImpitError { + pub fn is_connect_error(&self) -> bool { + matches!(self, ImpitError::ConnectError(_)) + } + pub fn from(error: reqwest::Error, context: Option) -> Self { let context = context.unwrap_or_default(); if error.is_timeout() { diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 1bd7a75c..ba43d156 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -22,10 +22,18 @@ use crate::{ pub struct Impit { pub(self) base_client: reqwest::Client, pub(self) h3_client: Option, + pub(self) vanilla_client: Option, h3_engine: Arc>>, config: ImpitBuilder, } +struct PreparedRequest { + method: Method, + url: Url, + headers: HeaderMap, + body: Option>, +} + impl Default for Impit { fn default() -> Self { ImpitBuilder::::default().build().unwrap() @@ -107,7 +115,7 @@ impl Default for ImpitBuilder Impit { })?; } + let vanilla_client = if config.vanilla_fallback && config.fingerprint.is_some() { + Some(Self::new_reqwest_client( + &ImpitBuilder:: { + fingerprint: None, + max_http_version: Version::HTTP_2, + ..config.clone() + }, + )?) + } else { + None + }; + // Set pseudo-header order from fingerprint or fall back to browser enum let pseudo_headers_order: Vec = if let Some(ref fingerprint) = config.fingerprint { fingerprint.http2.pseudo_header_order.to_vec() @@ -329,6 +349,7 @@ impl Impit { Ok(Impit { base_client, h3_client, + vanilla_client, config, h3_engine: Arc::new(RwLock::new(None)), }) @@ -401,6 +422,32 @@ impl Impit { } } + async fn execute_request( + &self, + client: &reqwest::Client, + prepared: &PreparedRequest, + timeout: Option, + h3: bool, + ) -> Result { + let mut req = client + .request(prepared.method.clone(), prepared.url.clone()) + .headers(prepared.headers.clone()); + + if h3 { + req = req.version(Version::HTTP_3); + } + + if let Some(t) = timeout { + req = req.timeout(t); + } + + if let Some(b) = prepared.body.clone() { + req = req.body(b); + } + + req.send().await + } + async fn send( &self, request: ImpitRequest, @@ -426,40 +473,32 @@ impl Impit { &self.base_client }; - let header_map: Result = HttpHeaders::from(request.headers).into(); + let header_map_result: Result = + HttpHeaders::from(request.headers).into(); + let header_map = header_map_result?; let method = Method::from_str(&request.method).map_err(|_| { ImpitError::InvalidMethod(format!("Invalid HTTP method: {}", request.method)) })?; - let mut client_request = client - .request(method.clone(), request.url.clone()) - .headers(header_map?); - - if h3 { - client_request = client_request.version(Version::HTTP_3); - } - - if let Some(timeout) = timeout { - client_request = client_request.timeout(timeout); - } + let max_redirects = match self.config.redirect { + RedirectBehavior::FollowRedirect(max) => max, + RedirectBehavior::ManualRedirect => 0, + }; - client_request = match request.body { - Some(body) => client_request.body(body), - None => client_request, + let prepared = PreparedRequest { + method: method.clone(), + url: request.url.clone(), + headers: header_map, + body: request.body, }; - let response = client_request.send().await; + let primary_result = self.execute_request(client, &prepared, timeout, h3).await; - let response = match response { + let response = match primary_result { Ok(resp) => resp, Err(err) => { - let max_redirects = match self.config.redirect { - RedirectBehavior::FollowRedirect(max) => max, - RedirectBehavior::ManualRedirect => 0, - }; - - return Err(ImpitError::from( + let primary_error = ImpitError::from( err, Some(ErrorContext { timeout: Some(timeout.unwrap_or(self.config.request_timeout)), @@ -468,7 +507,26 @@ impl Impit { protocol: Some(request.url.scheme().to_string()), url: Some(url.clone()), }), - )); + ); + + let fallback_client = self + .vanilla_client + .as_ref() + .filter(|_| primary_error.is_connect_error()); + let Some(vanilla_client) = fallback_client else { + return Err(primary_error); + }; + + debug!( + "Primary request to {url} failed with {primary_error}, retrying with vanilla client" + ); + match self + .execute_request(vanilla_client, &prepared, timeout, false) + .await + { + Ok(resp) => resp, + Err(_) => return Err(primary_error), + } } };