Skip to content

Commit c05b09d

Browse files
authored
feat: return the vanillaFallback option as an alternative for failing requests (#441)
The `vanillaFallback` option has been noop in a few of the latest versions of `impit`. The changes from this PR return this feature to support, e.g., servers with old TLS stacks that uncover some of the emulation discrepancies and cause the requests to fail. Closes #205
1 parent 0c46feb commit c05b09d

2 files changed

Lines changed: 87 additions & 25 deletions

File tree

impit/src/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ impl From<reqwest::Error> for ImpitError {
105105
}
106106

107107
impl ImpitError {
108+
pub fn is_connect_error(&self) -> bool {
109+
matches!(self, ImpitError::ConnectError(_))
110+
}
111+
108112
pub fn from(error: reqwest::Error, context: Option<ErrorContext>) -> Self {
109113
let context = context.unwrap_or_default();
110114
if error.is_timeout() {

impit/src/impit.rs

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ use crate::{
2222
pub struct Impit<CookieStoreImpl: CookieStore + 'static> {
2323
pub(self) base_client: reqwest::Client,
2424
pub(self) h3_client: Option<reqwest::Client>,
25+
pub(self) vanilla_client: Option<reqwest::Client>,
2526
h3_engine: Arc<RwLock<Option<H3Engine>>>,
2627
config: ImpitBuilder<CookieStoreImpl>,
2728
}
2829

30+
struct PreparedRequest {
31+
method: Method,
32+
url: Url,
33+
headers: HeaderMap,
34+
body: Option<Vec<u8>>,
35+
}
36+
2937
impl<CookieStoreImpl: CookieStore + 'static> Default for Impit<CookieStoreImpl> {
3038
fn default() -> Self {
3139
ImpitBuilder::<CookieStoreImpl>::default().build().unwrap()
@@ -107,7 +115,7 @@ impl<CookieStoreImpl: CookieStore + 'static> Default for ImpitBuilder<CookieStor
107115
ImpitBuilder {
108116
fingerprint: None,
109117
ignore_tls_errors: false,
110-
vanilla_fallback: true,
118+
vanilla_fallback: false,
111119
proxy_url: String::new(),
112120
request_timeout: Duration::from_secs(30),
113121
max_http_version: Version::HTTP_2,
@@ -312,6 +320,18 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
312320
})?;
313321
}
314322

323+
let vanilla_client = if config.vanilla_fallback && config.fingerprint.is_some() {
324+
Some(Self::new_reqwest_client(
325+
&ImpitBuilder::<CookieStoreImpl> {
326+
fingerprint: None,
327+
max_http_version: Version::HTTP_2,
328+
..config.clone()
329+
},
330+
)?)
331+
} else {
332+
None
333+
};
334+
315335
// Set pseudo-header order from fingerprint or fall back to browser enum
316336
let pseudo_headers_order: Vec<String> = if let Some(ref fingerprint) = config.fingerprint {
317337
fingerprint.http2.pseudo_header_order.to_vec()
@@ -329,6 +349,7 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
329349
Ok(Impit {
330350
base_client,
331351
h3_client,
352+
vanilla_client,
332353
config,
333354
h3_engine: Arc::new(RwLock::new(None)),
334355
})
@@ -401,6 +422,32 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
401422
}
402423
}
403424

425+
async fn execute_request(
426+
&self,
427+
client: &reqwest::Client,
428+
prepared: &PreparedRequest,
429+
timeout: Option<Duration>,
430+
h3: bool,
431+
) -> Result<Response, reqwest::Error> {
432+
let mut req = client
433+
.request(prepared.method.clone(), prepared.url.clone())
434+
.headers(prepared.headers.clone());
435+
436+
if h3 {
437+
req = req.version(Version::HTTP_3);
438+
}
439+
440+
if let Some(t) = timeout {
441+
req = req.timeout(t);
442+
}
443+
444+
if let Some(b) = prepared.body.clone() {
445+
req = req.body(b);
446+
}
447+
448+
req.send().await
449+
}
450+
404451
async fn send(
405452
&self,
406453
request: ImpitRequest,
@@ -426,40 +473,32 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
426473
&self.base_client
427474
};
428475

429-
let header_map: Result<HeaderMap, ImpitError> = HttpHeaders::from(request.headers).into();
476+
let header_map_result: Result<HeaderMap, ImpitError> =
477+
HttpHeaders::from(request.headers).into();
478+
let header_map = header_map_result?;
430479

431480
let method = Method::from_str(&request.method).map_err(|_| {
432481
ImpitError::InvalidMethod(format!("Invalid HTTP method: {}", request.method))
433482
})?;
434483

435-
let mut client_request = client
436-
.request(method.clone(), request.url.clone())
437-
.headers(header_map?);
438-
439-
if h3 {
440-
client_request = client_request.version(Version::HTTP_3);
441-
}
442-
443-
if let Some(timeout) = timeout {
444-
client_request = client_request.timeout(timeout);
445-
}
484+
let max_redirects = match self.config.redirect {
485+
RedirectBehavior::FollowRedirect(max) => max,
486+
RedirectBehavior::ManualRedirect => 0,
487+
};
446488

447-
client_request = match request.body {
448-
Some(body) => client_request.body(body),
449-
None => client_request,
489+
let prepared = PreparedRequest {
490+
method: method.clone(),
491+
url: request.url.clone(),
492+
headers: header_map,
493+
body: request.body,
450494
};
451495

452-
let response = client_request.send().await;
496+
let primary_result = self.execute_request(client, &prepared, timeout, h3).await;
453497

454-
let response = match response {
498+
let response = match primary_result {
455499
Ok(resp) => resp,
456500
Err(err) => {
457-
let max_redirects = match self.config.redirect {
458-
RedirectBehavior::FollowRedirect(max) => max,
459-
RedirectBehavior::ManualRedirect => 0,
460-
};
461-
462-
return Err(ImpitError::from(
501+
let primary_error = ImpitError::from(
463502
err,
464503
Some(ErrorContext {
465504
timeout: Some(timeout.unwrap_or(self.config.request_timeout)),
@@ -468,7 +507,26 @@ impl<CookieStoreImpl: CookieStore + 'static> Impit<CookieStoreImpl> {
468507
protocol: Some(request.url.scheme().to_string()),
469508
url: Some(url.clone()),
470509
}),
471-
));
510+
);
511+
512+
let fallback_client = self
513+
.vanilla_client
514+
.as_ref()
515+
.filter(|_| primary_error.is_connect_error());
516+
let Some(vanilla_client) = fallback_client else {
517+
return Err(primary_error);
518+
};
519+
520+
debug!(
521+
"Primary request to {url} failed with {primary_error}, retrying with vanilla client"
522+
);
523+
match self
524+
.execute_request(vanilla_client, &prepared, timeout, false)
525+
.await
526+
{
527+
Ok(resp) => resp,
528+
Err(_) => return Err(primary_error),
529+
}
472530
}
473531
};
474532

0 commit comments

Comments
 (0)