Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions impit/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ impl From<reqwest::Error> for ImpitError {
}

impl ImpitError {
pub fn is_connect_error(&self) -> bool {
matches!(self, ImpitError::ConnectError(_))
}

pub fn from(error: reqwest::Error, context: Option<ErrorContext>) -> Self {
let context = context.unwrap_or_default();
if error.is_timeout() {
Expand Down
108 changes: 83 additions & 25 deletions impit/src/impit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ use crate::{
pub struct Impit<CookieStoreImpl: CookieStore + 'static> {
pub(self) base_client: reqwest::Client,
pub(self) h3_client: Option<reqwest::Client>,
pub(self) vanilla_client: Option<reqwest::Client>,
h3_engine: Arc<RwLock<Option<H3Engine>>>,
config: ImpitBuilder<CookieStoreImpl>,
}

struct PreparedRequest {
method: Method,
url: Url,
headers: HeaderMap,
body: Option<Vec<u8>>,
}

impl<CookieStoreImpl: CookieStore + 'static> Default for Impit<CookieStoreImpl> {
fn default() -> Self {
ImpitBuilder::<CookieStoreImpl>::default().build().unwrap()
Expand Down Expand Up @@ -107,7 +115,7 @@ impl<CookieStoreImpl: CookieStore + 'static> Default for ImpitBuilder<CookieStor
ImpitBuilder {
fingerprint: None,
ignore_tls_errors: false,
vanilla_fallback: true,
vanilla_fallback: false,
proxy_url: String::new(),
request_timeout: Duration::from_secs(30),
max_http_version: Version::HTTP_2,
Expand Down Expand Up @@ -312,6 +320,18 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
})?;
}

let vanilla_client = if config.vanilla_fallback && config.fingerprint.is_some() {
Some(Self::new_reqwest_client(
&ImpitBuilder::<CookieStoreImpl> {
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<String> = if let Some(ref fingerprint) = config.fingerprint {
fingerprint.http2.pseudo_header_order.to_vec()
Expand All @@ -329,6 +349,7 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
Ok(Impit {
base_client,
h3_client,
vanilla_client,
config,
h3_engine: Arc::new(RwLock::new(None)),
})
Expand Down Expand Up @@ -401,6 +422,32 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
}
}

async fn execute_request(
&self,
client: &reqwest::Client,
prepared: &PreparedRequest,
timeout: Option<Duration>,
h3: bool,
) -> Result<Response, reqwest::Error> {
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,
Expand All @@ -426,40 +473,32 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
&self.base_client
};

let header_map: Result<HeaderMap, ImpitError> = HttpHeaders::from(request.headers).into();
let header_map_result: Result<HeaderMap, ImpitError> =
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,
Comment on lines +489 to +493
};

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)),
Expand All @@ -468,7 +507,26 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
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),
}
}
};

Expand Down
Loading