1#![allow(deprecated)]
5#![warn(missing_docs)]
141
142use http::header::USER_AGENT;
143pub use inventory;
144pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
145use std::error::Error;
146use std::sync::Mutex;
147use std::time::Instant;
148
149pub mod registry;
150
151use crate::path::RequestPath;
152use async_trait::async_trait;
153use bytes::Bytes;
154use cfg_if::cfg_if;
155use http::{
156 HeaderMap,
157 header::{ACCEPT, CONTENT_TYPE},
158};
159use itertools::Itertools;
160use mime::Mime;
161use reqwest::{RequestBuilder, Response, header::HeaderValue};
162use serde::{Deserialize, Serialize, de::DeserializeOwned};
163#[cfg(not(target_arch = "wasm32"))]
164use std::io::ErrorKind;
165use std::{
166 fmt::Display,
167 sync::atomic::{AtomicUsize, Ordering},
168 time::Duration,
169};
170use thiserror::Error;
171use tracing::{debug, instrument, warn};
172
173use std::sync::{Arc, LazyLock};
174
175#[cfg(feature = "tunneling")]
176mod fronted;
177#[cfg(feature = "tunneling")]
178pub use fronted::FrontPolicy;
179mod url;
180pub use url::{IntoUrl, Url};
181mod user_agent;
182pub use user_agent::UserAgent;
183
184#[cfg(not(target_arch = "wasm32"))]
185pub mod dns;
186mod path;
187
188#[cfg(not(target_arch = "wasm32"))]
189pub use dns::{HickoryDnsResolver, ResolveError};
190
191#[cfg(not(target_arch = "wasm32"))]
193use crate::registry::default_builder;
194#[doc(hidden)]
195pub use nym_bin_common::bin_info;
196#[cfg(not(target_arch = "wasm32"))]
197use nym_http_api_client_macro::client_defaults;
198
199pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
204
205const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
206
207#[cfg(not(target_arch = "wasm32"))]
208client_defaults!(
209 priority = -100;
210 gzip = true,
211 deflate = true,
212 brotli = true,
213 zstd = true,
214 timeout = DEFAULT_TIMEOUT,
215 user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
216);
217
218static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
219 tracing::info!("Initializing shared HTTP client");
220 cfg_if! {
221 if #[cfg(target_arch = "wasm32")] {
222 reqwest::ClientBuilder::new().build()
223 .expect("failed to initialize shared http client")
224 } else {
225 let mut builder = default_builder();
226
227 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
228
229 builder
230 .build()
231 .expect("failed to initialize shared http client")
232 }
233 }
234});
235
236pub(crate) static SHARED_NETWORK_RECONFIGURATION: LazyLock<Arc<Mutex<Option<Instant>>>> =
237 LazyLock::new(|| Arc::new(Mutex::new(None)));
238
239pub fn network_reconfigured() {
241 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap() = Some(Instant::now());
242}
243
244pub type PathSegments<'a> = &'a [&'a str];
246pub type Params<'a, K, V> = &'a [(K, V)];
248
249pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum SerializationFormat {
255 Json,
257 Bincode,
259 Yaml,
261 Text,
263}
264
265impl Display for SerializationFormat {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 SerializationFormat::Json => write!(f, "json"),
269 SerializationFormat::Bincode => write!(f, "bincode"),
270 SerializationFormat::Yaml => write!(f, "yaml"),
271 SerializationFormat::Text => write!(f, "text"),
272 }
273 }
274}
275
276impl SerializationFormat {
277 #[allow(missing_docs)]
278 pub fn content_type(&self) -> String {
279 match self {
280 SerializationFormat::Json => "application/json".to_string(),
281 SerializationFormat::Bincode => "application/bincode".to_string(),
282 SerializationFormat::Yaml => "application/yaml".to_string(),
283 SerializationFormat::Text => "text/plain".to_string(),
284 }
285 }
286}
287
288#[allow(missing_docs)]
289#[derive(Debug)]
290pub struct ReqwestErrorWrapper(reqwest::Error);
291
292impl Display for ReqwestErrorWrapper {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 cfg_if::cfg_if! {
295 if #[cfg(not(target_arch = "wasm32"))] {
296 if self.0.is_connect() {
297 write!(f, "failed to connect: ")?;
298 }
299 }
300 }
301
302 if self.0.is_timeout() {
303 write!(f, "timed out: ")?;
304 }
305 if self.0.is_redirect()
306 && let Some(final_stop) = self.0.url()
307 {
308 write!(f, "redirect loop at {final_stop}: ")?;
309 }
310
311 self.0.fmt(f)?;
312 if let Some(status_code) = self.0.status() {
313 write!(f, " status: {status_code}")?;
314 } else {
315 write!(f, " unknown status code")?;
316 }
317
318 if let Some(source) = self.0.source() {
319 write!(f, " source: {source}")?;
320 } else {
321 write!(f, " unknown lower-level error source")?;
322 }
323
324 Ok(())
325 }
326}
327
328impl std::error::Error for ReqwestErrorWrapper {}
329
330#[derive(Debug, Error)]
332#[allow(missing_docs)]
333pub enum HttpClientError {
334 #[error("did not provide any valid client URLs")]
335 NoUrlsProvided,
336
337 #[error("failed to construct inner reqwest client: {source}")]
338 ReqwestBuildError {
339 #[source]
340 source: reqwest::Error,
341 },
342
343 #[deprecated(
344 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
345 )]
346 #[error("request failed with error message: {0}")]
347 GenericRequestFailure(String),
348
349 #[deprecated(
350 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
351 )]
352 #[error("there was an issue with the REST request: {source}")]
353 ReqwestClientError {
354 #[from]
355 source: reqwest::Error,
356 },
357
358 #[error("failed to parse {raw} as a valid URL: {source}")]
359 MalformedUrl {
360 raw: String,
361 #[source]
362 source: reqwest::Error,
363 },
364
365 #[error("failed to parse header value: {source}")]
366 InvalidHeaderValue {
367 #[source]
368 source: http::Error,
369 },
370
371 #[error("failed to send request for {url}: {source}")]
372 RequestSendFailure {
373 url: Box<reqwest::Url>,
374 #[source]
375 source: ReqwestErrorWrapper,
376 },
377
378 #[error("failed to read response body from {url}: {source}")]
379 ResponseReadFailure {
380 url: Box<reqwest::Url>,
381 headers: Box<HeaderMap>,
382 status: StatusCode,
383 #[source]
384 source: ReqwestErrorWrapper,
385 },
386
387 #[error("failed to deserialize received response: {source}")]
388 ResponseDeserialisationFailure { source: serde_json::Error },
389
390 #[error("provided url is malformed: {source}")]
391 UrlParseFailure {
392 #[from]
393 source: url::ParseError,
394 },
395
396 #[error("the requested resource could not be found at {url}")]
397 NotFound { url: Box<reqwest::Url> },
398
399 #[error("attempted to use domain fronting and clone a request containing stream data")]
400 AttemptedToCloneStreamRequest,
401
402 #[error(
406 "the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
407 )]
408 RequestFailure {
409 url: Box<reqwest::Url>,
410 status: StatusCode,
411 headers: Box<HeaderMap>,
412 },
413
414 #[error(
415 "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
416 )]
417 EmptyResponse {
418 url: Box<reqwest::Url>,
419 status: StatusCode,
420 headers: Box<HeaderMap>,
421 },
422
423 #[error(
424 "failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
425 )]
426 EndpointFailure {
427 url: Box<reqwest::Url>,
428 status: StatusCode,
429 headers: Box<HeaderMap>,
430 error: String,
431 },
432
433 #[error("failed to decode response body: {message} from {content}")]
434 ResponseDecodeFailure { message: String, content: String },
435
436 #[error("failed to resolve request to {url} due to data inconsistency: {details}")]
437 InternalResponseInconsistency { url: ::url::Url, details: String },
438
439 #[cfg(not(target_arch = "wasm32"))]
440 #[error("encountered dns failure: {inner}")]
441 DnsLookupFailure {
442 #[from]
443 inner: ResolveError,
444 },
445
446 #[error("Failed to encode bincode: {0}")]
447 Bincode(#[from] bincode::Error),
448
449 #[error("Failed to json: {0}")]
450 Json(#[from] serde_json::Error),
451
452 #[error("Failed to yaml: {0}")]
453 Yaml(#[from] serde_yaml::Error),
454
455 #[error("Failed to plain: {0}")]
456 Plain(#[from] serde_plain::Error),
457
458 #[cfg(target_arch = "wasm32")]
459 #[error("the request has timed out")]
460 RequestTimeout,
461}
462
463#[allow(missing_docs)]
464#[allow(deprecated)]
465impl HttpClientError {
466 pub fn is_timeout(&self) -> bool {
468 match self {
469 HttpClientError::ReqwestClientError { source } => source.is_timeout(),
470 HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
471 HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
472 #[cfg(not(target_arch = "wasm32"))]
473 HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
474 #[cfg(target_arch = "wasm32")]
475 HttpClientError::RequestTimeout => true,
476 _ => false,
477 }
478 }
479
480 pub fn status_code(&self) -> Option<StatusCode> {
482 match self {
483 HttpClientError::ResponseReadFailure { status, .. } => Some(*status),
484 HttpClientError::RequestFailure { status, .. } => Some(*status),
485 HttpClientError::EmptyResponse { status, .. } => Some(*status),
486 HttpClientError::EndpointFailure { status, .. } => Some(*status),
487 _ => None,
488 }
489 }
490
491 pub fn reqwest_client_build_error(source: reqwest::Error) -> Self {
492 HttpClientError::ReqwestBuildError { source }
493 }
494
495 pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
496 HttpClientError::RequestSendFailure {
497 url: Box::new(url),
498 source: ReqwestErrorWrapper(source),
499 }
500 }
501
502 pub fn is_data_inconsistency(&self) -> bool {
503 matches!(self, HttpClientError::InternalResponseInconsistency { .. })
504 }
505}
506
507#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
513#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
514pub trait ApiClientCore {
515 fn create_request<P, B, K, V>(
517 &self,
518 method: reqwest::Method,
519 path: P,
520 params: Params<'_, K, V>,
521 body: Option<&B>,
522 ) -> Result<RequestBuilder, HttpClientError>
523 where
524 P: RequestPath,
525 B: Serialize + ?Sized,
526 K: AsRef<str>,
527 V: AsRef<str>;
528
529 fn create_request_endpoint<B, S>(
545 &self,
546 method: reqwest::Method,
547 endpoint: S,
548 body: Option<&B>,
549 ) -> Result<RequestBuilder, HttpClientError>
550 where
551 B: Serialize + ?Sized,
552 S: AsRef<str>,
553 {
554 let mut standin_url: Url = "http://example.com".parse().unwrap();
559
560 match endpoint.as_ref().split_once("?") {
561 Some((path, query)) => {
562 standin_url.set_path(path);
563 standin_url.set_query(Some(query));
564 }
565 None => standin_url.set_path(endpoint.as_ref()),
567 }
568
569 let path: Vec<&str> = match standin_url.path_segments() {
570 Some(segments) => segments.collect(),
571 None => Vec::new(),
572 };
573 let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
574
575 self.create_request(method, path.as_slice(), ¶ms, body)
576 }
577
578 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError>;
584
585 async fn send_request<P, B, K, V>(
587 &self,
588 method: reqwest::Method,
589 path: P,
590 params: Params<'_, K, V>,
591 json_body: Option<&B>,
592 ) -> Result<Response, HttpClientError>
593 where
594 P: RequestPath + Send + Sync,
595 B: Serialize + ?Sized + Sync,
596 K: AsRef<str> + Sync,
597 V: AsRef<str> + Sync,
598 {
599 let req = self.create_request(method, path, params, json_body)?;
600 self.send(req).await
601 }
602
603 fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
610
611 #[cfg(feature = "tunneling")]
614 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
615}
616
617pub struct ClientBuilder {
620 urls: Vec<Url>,
621
622 timeout: Option<Duration>,
623 custom_user_agent: Option<HeaderValue>,
624 reqwest_client_builder: Option<reqwest::ClientBuilder>,
625 #[allow(dead_code)] use_secure_dns: bool,
627
628 #[cfg(feature = "tunneling")]
629 front: fronted::Front,
630
631 retry_limit: usize,
632 serialization: SerializationFormat,
633
634 error: Option<HttpClientError>,
635}
636
637impl ClientBuilder {
638 pub fn new<U>(url: U) -> Result<Self, HttpClientError>
642 where
643 U: IntoUrl,
644 {
645 let str_url = url.as_str();
646
647 if !str_url.starts_with("http") {
649 let alt = format!("http://{str_url}");
650 warn!(
651 "the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."
652 );
653 Self::new(alt)
655 } else {
656 let url = url.to_url()?;
657 Self::new_with_urls(vec![url])
658 }
659 }
660
661 #[cfg(feature = "network-defaults")]
663 #[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
666 pub fn from_network(
667 network: &nym_network_defaults::NymNetworkDetails,
668 ) -> Result<Self, HttpClientError> {
669 let urls = network.nym_api_urls.as_ref().cloned().unwrap_or_default();
670 Self::new_with_fronted_urls(urls.clone())
671 }
672
673 #[cfg(feature = "network-defaults")]
675 pub fn new_with_fronted_urls(
676 urls: Vec<nym_network_defaults::ApiUrl>,
677 ) -> Result<Self, HttpClientError> {
678 let urls = urls
679 .into_iter()
680 .map(|api_url| {
681 let mut url = Url::parse(&api_url.url)?;
683
684 #[cfg(feature = "tunneling")]
686 if let Some(ref front_hosts) = api_url.front_hosts {
687 let fronts: Vec<String> = front_hosts
688 .iter()
689 .map(|host| format!("https://{}", host))
690 .collect();
691 url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
692 HttpClientError::MalformedUrl {
693 raw: api_url.url.clone(),
694 source,
695 }
696 })?;
697 }
698
699 Ok(url)
700 })
701 .collect::<Result<Vec<_>, HttpClientError>>()?;
702
703 let mut builder = Self::new_with_urls(urls)?;
704
705 #[cfg(feature = "tunneling")]
707 {
708 builder = builder.with_fronting(None);
709 }
710
711 Ok(builder)
712 }
713
714 pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
716 if urls.is_empty() {
717 return Err(HttpClientError::NoUrlsProvided);
718 }
719
720 let urls = Self::check_urls(urls);
721
722 Ok(ClientBuilder {
723 urls,
724 timeout: None,
725 custom_user_agent: None,
726 reqwest_client_builder: None,
727 use_secure_dns: true,
728 #[cfg(feature = "tunneling")]
729 front: fronted::Front::off(),
730
731 retry_limit: 0,
732 serialization: SerializationFormat::Json,
733 error: None,
734 })
735 }
736
737 #[cfg(not(target_arch = "wasm32"))]
740 pub fn non_shared(mut self) -> Self {
741 if self.reqwest_client_builder.is_none() {
742 self.reqwest_client_builder = Some(default_builder());
743 }
744 self
745 }
746
747 pub fn add_url(mut self, url: Url) -> Self {
749 self.urls.push(url);
750 self
751 }
752
753 fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
754 urls = urls.into_iter().unique().collect();
756
757 urls.iter()
759 .filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
760 .for_each(|url| {
761 warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
762 });
763
764 urls
765 }
766
767 pub fn with_timeout(mut self, timeout: Duration) -> Self {
773 self.timeout = Some(timeout);
774 self
775 }
776
777 pub fn with_retries(mut self, retry_limit: usize) -> Self {
785 self.retry_limit = retry_limit;
786 self
787 }
788
789 pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
791 self.reqwest_client_builder = Some(reqwest_builder);
792 self
793 }
794
795 pub fn with_user_agent<V>(mut self, value: V) -> Self
797 where
798 V: TryInto<HeaderValue>,
799 V::Error: Into<http::Error>,
800 {
801 match value.try_into() {
802 Ok(v) => self.custom_user_agent = Some(v),
803 Err(err) => {
804 self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
805 }
806 }
807 self
808 }
809
810 pub fn with_serialization(mut self, format: SerializationFormat) -> Self {
812 self.serialization = format;
813 self
814 }
815
816 pub fn with_bincode(self) -> Self {
818 self.with_serialization(SerializationFormat::Bincode)
819 }
820
821 pub fn build(self) -> Result<Client, HttpClientError> {
823 if let Some(err) = self.error {
824 return Err(err);
825 }
826
827 #[cfg(target_arch = "wasm32")]
828 let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
829
830 #[cfg(not(target_arch = "wasm32"))]
831 let reqwest_client = self
832 .reqwest_client_builder
833 .map(|mut builder| {
834 if self.use_secure_dns {
836 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
837 }
838
839 builder
840 .build()
841 .map_err(HttpClientError::reqwest_client_build_error)
842 })
843 .transpose()?;
844
845 let client = Client {
846 base_urls: self.urls,
847 current_idx: Arc::new(AtomicUsize::new(0)),
848 reqwest_client,
849 custom_user_agent: self.custom_user_agent,
850
851 #[cfg(feature = "tunneling")]
852 front: self.front,
853
854 #[cfg(target_arch = "wasm32")]
855 request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
856 retry_limit: self.retry_limit,
857 serialization: self.serialization,
858 };
859
860 Ok(client)
861 }
862}
863
864#[derive(Debug, Clone)]
866pub struct Client {
867 base_urls: Vec<Url>,
868 current_idx: Arc<AtomicUsize>,
869 reqwest_client: Option<reqwest::Client>,
870 custom_user_agent: Option<HeaderValue>,
871
872 #[cfg(feature = "tunneling")]
873 front: fronted::Front,
874
875 #[cfg(target_arch = "wasm32")]
876 request_timeout: Duration,
877
878 retry_limit: usize,
879 serialization: SerializationFormat,
880}
881
882impl Client {
883 pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
889 Self::new_url(base_url, timeout).expect(
890 "we provided valid url and we were unwrapping previous construction errors anyway",
891 )
892 }
893
894 pub fn new_url<U>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError>
896 where
897 U: IntoUrl,
898 {
899 let builder = Self::builder(url)?;
900 match timeout {
901 Some(timeout) => builder.with_timeout(timeout).build(),
902 None => builder.build(),
903 }
904 }
905
906 pub fn builder<U>(url: U) -> Result<ClientBuilder, HttpClientError>
910 where
911 U: IntoUrl,
912 {
913 ClientBuilder::new(url)
914 }
915
916 pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
918 self.current_idx.store(0, Ordering::Relaxed);
919 self.base_urls = new_urls
920 }
921
922 pub fn clone_with_new_url(&self, new_url: Url) -> Self {
924 Client {
925 base_urls: vec![new_url],
926 current_idx: Arc::new(Default::default()),
927 reqwest_client: None,
928 custom_user_agent: None,
929
930 #[cfg(feature = "tunneling")]
931 front: self.front.clone(),
932 retry_limit: self.retry_limit,
933
934 #[cfg(target_arch = "wasm32")]
935 request_timeout: self.request_timeout,
936 serialization: self.serialization,
937 }
938 }
939
940 pub fn current_url(&self) -> &Url {
942 &self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
943 }
944
945 pub fn base_urls(&self) -> &[Url] {
947 &self.base_urls
948 }
949
950 pub fn base_urls_mut(&mut self) -> &mut [Url] {
952 &mut self.base_urls
953 }
954
955 pub fn change_retry_limit(&mut self, limit: usize) {
957 self.retry_limit = limit;
958 }
959
960 #[cfg(feature = "tunneling")]
961 fn matches_current_host(&self, url: &Url) -> bool {
962 if self.front.is_enabled() {
963 url.host_str() == self.current_url().front_str()
964 } else {
965 url.host_str() == self.current_url().host_str()
966 }
967 }
968
969 #[cfg(not(feature = "tunneling"))]
970 fn matches_current_host(&self, url: &Url) -> bool {
971 url.host_str() == self.current_url().host_str()
972 }
973
974 fn update_host(&self, maybe_url: Option<Url>) {
981 if let Some(err_url) = maybe_url
983 && !self.matches_current_host(&err_url)
984 {
985 return;
986 }
987
988 #[cfg(feature = "tunneling")]
989 if self.front.is_enabled() {
990 let url = self.current_url();
992
993 if url.has_front() && !url.update() {
996 return;
998 }
999 }
1000
1001 if self.base_urls.len() > 1 {
1002 let orig = self.current_idx.load(Ordering::Relaxed);
1003
1004 #[allow(unused_mut)]
1005 let mut next = (orig + 1) % self.base_urls.len();
1006
1007 #[cfg(feature = "tunneling")]
1009 if self.front.is_enabled() {
1010 while next != orig {
1011 if self.base_urls[next].has_front() {
1012 break;
1014 }
1015
1016 next = (next + 1) % self.base_urls.len();
1017 }
1018 }
1019
1020 self.current_idx.store(next, Ordering::Relaxed);
1021 debug!(
1022 "http client rotating host {} -> {}",
1023 self.base_urls[orig], self.base_urls[next]
1024 );
1025 }
1026 }
1027
1028 pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
1041 let url = self.current_url();
1042 r.url_mut().set_host(url.host_str()).unwrap();
1043
1044 #[cfg(feature = "tunneling")]
1045 if self.front.is_enabled() {
1046 if let Some(front_host) = url.front_str() {
1047 if let Some(actual_host) = url.host_str() {
1048 tracing::debug!(
1049 "Domain fronting enabled: routing via CDN {} to actual host {}",
1050 front_host,
1051 actual_host
1052 );
1053
1054 r.url_mut().set_host(Some(front_host)).unwrap();
1056
1057 let actual_host_header: HeaderValue =
1058 actual_host.parse().unwrap_or(HeaderValue::from_static(""));
1059 _ = r
1062 .headers_mut()
1063 .insert(reqwest::header::HOST, actual_host_header);
1064
1065 let front_host_header: HeaderValue =
1067 front_host.parse().unwrap_or(HeaderValue::from_static(""));
1068 _ = r
1069 .headers_mut()
1070 .insert(NYM_OUTER_SNI_HEADER, front_host_header);
1071
1072 return (url.as_str(), url.front_str());
1073 } else {
1074 tracing::debug!(
1075 "Domain fronting is enabled, but no host_url is defined for current URL"
1076 )
1077 }
1078 } else {
1079 tracing::debug!(
1080 "Domain fronting is enabled, but current URL has no front_hosts configured"
1081 )
1082 }
1083 }
1084 (url.as_str(), None)
1085 }
1086}
1087
1088#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1089#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1090impl ApiClientCore for Client {
1091 #[instrument(level = "debug", skip_all, fields(path=?path))]
1092 fn create_request<P, B, K, V>(
1093 &self,
1094 method: reqwest::Method,
1095 path: P,
1096 params: Params<'_, K, V>,
1097 body: Option<&B>,
1098 ) -> Result<RequestBuilder, HttpClientError>
1099 where
1100 P: RequestPath,
1101 B: Serialize + ?Sized,
1102 K: AsRef<str>,
1103 V: AsRef<str>,
1104 {
1105 let url = self.current_url();
1106 let url = sanitize_url(url, path, params);
1107
1108 let mut req = reqwest::Request::new(method, url.into());
1109
1110 self.apply_hosts_to_req(&mut req);
1111
1112 let client = if let Some(client) = &self.reqwest_client {
1113 client.clone()
1114 } else {
1115 SHARED_CLIENT.clone()
1116 };
1117 let mut rb = RequestBuilder::from_parts(client, req);
1118
1119 rb = rb
1120 .header(ACCEPT, self.serialization.content_type())
1121 .header(CONTENT_TYPE, self.serialization.content_type());
1122
1123 if let Some(user_agent) = &self.custom_user_agent {
1124 rb = rb.header(USER_AGENT, user_agent.clone());
1125 }
1126
1127 if let Some(body) = body {
1128 match self.serialization {
1129 SerializationFormat::Json => {
1130 rb = rb.json(body);
1131 }
1132 SerializationFormat::Bincode => {
1133 let body = bincode::serialize(body)?;
1134 rb = rb.body(body);
1135 }
1136 SerializationFormat::Yaml => {
1137 let mut body_bytes = Vec::new();
1138 serde_yaml::to_writer(&mut body_bytes, &body)?;
1139 rb = rb.body(body_bytes);
1140 }
1141 SerializationFormat::Text => {
1142 let body = serde_plain::to_string(&body)?.as_bytes().to_vec();
1143 rb = rb.body(body);
1144 }
1145 }
1146 }
1147
1148 Ok(rb)
1149 }
1150
1151 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError> {
1152 let mut attempts = 0;
1153 loop {
1154 let r = request
1156 .try_clone()
1157 .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?;
1158
1159 let mut req = r
1162 .build()
1163 .map_err(HttpClientError::reqwest_client_build_error)?;
1164 self.apply_hosts_to_req(&mut req);
1165 let url: Url = req.url().clone().into();
1166
1167 let request_start = Instant::now();
1168
1169 #[cfg(target_arch = "wasm32")]
1170 let response: Result<Response, HttpClientError> = {
1171 let client = self
1172 .reqwest_client
1173 .as_ref()
1174 .unwrap_or_else(|| &*SHARED_CLIENT);
1175 Ok(
1176 wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
1177 .await
1178 .map_err(|_timeout| HttpClientError::RequestTimeout)??,
1179 )
1180 };
1181
1182 #[cfg(not(target_arch = "wasm32"))]
1183 let response = {
1184 let client = self
1185 .reqwest_client
1186 .as_ref()
1187 .unwrap_or_else(|| &*SHARED_CLIENT);
1188 client.execute(req).await
1189 };
1190
1191 match response {
1192 Ok(resp) => {
1193 if is_http_rate_limit_err(&resp) {
1195 warn!("encountered vercel rate limit error for {}", url.as_str());
1196 self.maybe_rotate_hosts(Some(url.clone()));
1198 }
1199
1200 return Ok(resp);
1201 }
1202 Err(err) => {
1203 let last_network_reconfiguration =
1204 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap();
1205 let network_reconfigured =
1206 last_network_reconfiguration.is_some_and(|last| last > request_start);
1207
1208 #[cfg(target_arch = "wasm32")]
1209 let is_network_err = err.is_timeout();
1210 #[cfg(not(target_arch = "wasm32"))]
1211 let is_network_err = might_be_network_interference(&err);
1212
1213 if is_network_err & !network_reconfigured {
1214 self.maybe_rotate_hosts(Some(url.clone()));
1216
1217 #[cfg(feature = "tunneling")]
1218 self.maybe_enable_fronting(("network", url.as_str(), &err));
1219 }
1220
1221 if attempts < self.retry_limit {
1222 attempts += 1;
1223 warn!(
1224 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1225 self.retry_limit
1226 );
1227 continue;
1228 }
1229
1230 cfg_if::cfg_if! {
1232 if #[cfg(target_arch = "wasm32")] {
1233 return Err(err);
1234 } else {
1235 return Err(HttpClientError::request_send_error(url.into(), err));
1236 }
1237 }
1238 }
1239 }
1240 }
1241 }
1242
1243 fn maybe_rotate_hosts(&self, offending: Option<Url>) {
1244 self.update_host(offending);
1245 }
1246
1247 #[cfg(feature = "tunneling")]
1248 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
1249 let was_enabled = self.front.is_enabled();
1252 self.front.retry_enable();
1253 if !was_enabled && self.front.is_enabled() {
1254 tracing::debug!("Domain fronting activated after failure: {context:?}",);
1255 }
1256 }
1257}
1258
1259const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
1260const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
1261
1262pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
1264 let status = resp.status() == StatusCode::FORBIDDEN;
1265 let header = resp
1266 .headers()
1267 .get(VERCEL_CHALLENGE_HEADER)
1268 .is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
1269 let content_type = resp
1270 .headers()
1271 .get(CONTENT_TYPE)
1272 .and_then(|value| value.to_str().ok())
1273 .and_then(|value| value.parse::<Mime>().ok())
1274 .is_some_and(|mime_type| {
1275 mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
1276 });
1277
1278 status && header && content_type
1279}
1280
1281#[cfg(not(target_arch = "wasm32"))]
1282const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
1283
1284#[cfg(not(target_arch = "wasm32"))]
1293pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
1294 if err.is_timeout() {
1295 return true;
1296 }
1297
1298 if !(err.is_connect() || err.is_request()) {
1299 return false;
1300 }
1301
1302 let mut inner = err.source();
1307 for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
1308 if let Some(e) = inner {
1309 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
1310 match io_err.kind() {
1312 ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
1314 ErrorKind::ConnectionReset
1316 | ErrorKind::HostUnreachable
1317 | ErrorKind::ConnectionRefused => return true,
1318 ErrorKind::Other | ErrorKind::InvalidData => {
1320 inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
1323 }
1324 _ => return false,
1325 }
1326 } else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
1327 return true;
1329 } else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
1330 return resolve_err.is_nx_domain();
1332 } else {
1333 inner = e.source();
1334 }
1335 } else {
1336 break;
1337 }
1338 }
1339
1340 false
1341}
1342
1343#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1347#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1348pub trait ApiClient: ApiClientCore {
1349 fn create_get_request<P, K, V>(
1351 &self,
1352 path: P,
1353 params: Params<'_, K, V>,
1354 ) -> Result<RequestBuilder, HttpClientError>
1355 where
1356 P: RequestPath,
1357 K: AsRef<str>,
1358 V: AsRef<str>,
1359 {
1360 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1361 }
1362
1363 fn create_post_request<P, B, K, V>(
1365 &self,
1366 path: P,
1367 params: Params<'_, K, V>,
1368 json_body: &B,
1369 ) -> Result<RequestBuilder, HttpClientError>
1370 where
1371 P: RequestPath,
1372 B: Serialize + ?Sized,
1373 K: AsRef<str>,
1374 V: AsRef<str>,
1375 {
1376 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1377 }
1378
1379 fn create_delete_request<P, K, V>(
1381 &self,
1382 path: P,
1383 params: Params<'_, K, V>,
1384 ) -> Result<RequestBuilder, HttpClientError>
1385 where
1386 P: RequestPath,
1387 K: AsRef<str>,
1388 V: AsRef<str>,
1389 {
1390 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1391 }
1392
1393 fn create_patch_request<P, B, K, V>(
1395 &self,
1396 path: P,
1397 params: Params<'_, K, V>,
1398 json_body: &B,
1399 ) -> Result<RequestBuilder, HttpClientError>
1400 where
1401 P: RequestPath,
1402 B: Serialize + ?Sized,
1403 K: AsRef<str>,
1404 V: AsRef<str>,
1405 {
1406 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1407 }
1408
1409 #[instrument(level = "debug", skip_all, fields(path=?path))]
1411 async fn send_get_request<P, K, V>(
1412 &self,
1413 path: P,
1414 params: Params<'_, K, V>,
1415 ) -> Result<Response, HttpClientError>
1416 where
1417 P: RequestPath + Send + Sync,
1418 K: AsRef<str> + Sync,
1419 V: AsRef<str> + Sync,
1420 {
1421 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1422 .await
1423 }
1424
1425 async fn send_post_request<P, B, K, V>(
1427 &self,
1428 path: P,
1429 params: Params<'_, K, V>,
1430 json_body: &B,
1431 ) -> Result<Response, HttpClientError>
1432 where
1433 P: RequestPath + Send + Sync,
1434 B: Serialize + ?Sized + Sync,
1435 K: AsRef<str> + Sync,
1436 V: AsRef<str> + Sync,
1437 {
1438 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1439 .await
1440 }
1441
1442 async fn send_delete_request<P, K, V>(
1444 &self,
1445 path: P,
1446 params: Params<'_, K, V>,
1447 ) -> Result<Response, HttpClientError>
1448 where
1449 P: RequestPath + Send + Sync,
1450 K: AsRef<str> + Sync,
1451 V: AsRef<str> + Sync,
1452 {
1453 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1454 .await
1455 }
1456
1457 async fn send_patch_request<P, B, K, V>(
1459 &self,
1460 path: P,
1461 params: Params<'_, K, V>,
1462 json_body: &B,
1463 ) -> Result<Response, HttpClientError>
1464 where
1465 P: RequestPath + Send + Sync,
1466 B: Serialize + ?Sized + Sync,
1467 K: AsRef<str> + Sync,
1468 V: AsRef<str> + Sync,
1469 {
1470 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1471 .await
1472 }
1473
1474 #[instrument(level = "debug", skip_all, fields(path=?path))]
1478 async fn get_json<P, T, K, V>(
1480 &self,
1481 path: P,
1482 params: Params<'_, K, V>,
1483 ) -> Result<T, HttpClientError>
1484 where
1485 P: RequestPath + Send + Sync,
1486 for<'a> T: Deserialize<'a>,
1487 K: AsRef<str> + Sync,
1488 V: AsRef<str> + Sync,
1489 {
1490 self.get_response(path, params).await
1491 }
1492
1493 async fn parse_response<T>(
1495 &self,
1496 res: Response,
1497 allow_empty: bool,
1498 ) -> Result<T, HttpClientError>
1499 where
1500 T: DeserializeOwned,
1501 {
1502 let url = Url::from(res.url());
1503 parse_response(res, allow_empty).await.inspect_err(|e| {
1504 if matches!(
1505 e,
1508 HttpClientError::ResponseReadFailure {
1509 url: _,
1510 headers: _,
1511 status: _,
1512 source: _,
1513 }
1514 ) {
1515 self.maybe_rotate_hosts(Some(url.clone()));
1516 #[cfg(feature = "tunneling")]
1517 self.maybe_enable_fronting(("parse/read", url.as_str(), e));
1518 }
1519 })
1520 }
1521
1522 #[instrument(level = "debug", skip_all, fields(path=?path))]
1526 async fn get_response<P, T, K, V>(
1527 &self,
1528 path: P,
1529 params: Params<'_, K, V>,
1530 ) -> Result<T, HttpClientError>
1531 where
1532 P: RequestPath + Send + Sync,
1533 for<'a> T: Deserialize<'a>,
1534 K: AsRef<str> + Sync,
1535 V: AsRef<str> + Sync,
1536 {
1537 let res = self
1538 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1539 .await?;
1540
1541 self.parse_response(res, false).await
1542 }
1543
1544 async fn post_json<P, B, T, K, V>(
1548 &self,
1549 path: P,
1550 params: Params<'_, K, V>,
1551 json_body: &B,
1552 ) -> Result<T, HttpClientError>
1553 where
1554 P: RequestPath + Send + Sync,
1555 B: Serialize + ?Sized + Sync,
1556 for<'a> T: Deserialize<'a>,
1557 K: AsRef<str> + Sync,
1558 V: AsRef<str> + Sync,
1559 {
1560 let res = self
1561 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1562 .await?;
1563 self.parse_response(res, false).await
1564 }
1565
1566 async fn delete_json<P, T, K, V>(
1570 &self,
1571 path: P,
1572 params: Params<'_, K, V>,
1573 ) -> Result<T, HttpClientError>
1574 where
1575 P: RequestPath + Send + Sync,
1576 for<'a> T: Deserialize<'a>,
1577 K: AsRef<str> + Sync,
1578 V: AsRef<str> + Sync,
1579 {
1580 let res = self
1581 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1582 .await?;
1583 self.parse_response(res, false).await
1584 }
1585
1586 async fn patch_json<P, B, T, K, V>(
1590 &self,
1591 path: P,
1592 params: Params<'_, K, V>,
1593 json_body: &B,
1594 ) -> Result<T, HttpClientError>
1595 where
1596 P: RequestPath + Send + Sync,
1597 B: Serialize + ?Sized + Sync,
1598 for<'a> T: Deserialize<'a>,
1599 K: AsRef<str> + Sync,
1600 V: AsRef<str> + Sync,
1601 {
1602 let res = self
1603 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1604 .await?;
1605 self.parse_response(res, false).await
1606 }
1607
1608 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1611 where
1612 for<'a> T: Deserialize<'a>,
1613 S: AsRef<str> + Sync + Send,
1614 {
1615 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1616 let res = self.send(req).await?;
1617 self.parse_response(res, false).await
1618 }
1619
1620 async fn post_json_data_to<B, T, S>(
1623 &self,
1624 endpoint: S,
1625 json_body: &B,
1626 ) -> Result<T, HttpClientError>
1627 where
1628 B: Serialize + ?Sized + Sync,
1629 for<'a> T: Deserialize<'a>,
1630 S: AsRef<str> + Sync + Send,
1631 {
1632 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1633 let res = self.send(req).await?;
1634 self.parse_response(res, false).await
1635 }
1636
1637 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1640 where
1641 for<'a> T: Deserialize<'a>,
1642 S: AsRef<str> + Sync + Send,
1643 {
1644 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1645 let res = self.send(req).await?;
1646 self.parse_response(res, false).await
1647 }
1648
1649 async fn patch_json_data_at<B, T, S>(
1652 &self,
1653 endpoint: S,
1654 json_body: &B,
1655 ) -> Result<T, HttpClientError>
1656 where
1657 B: Serialize + ?Sized + Sync,
1658 for<'a> T: Deserialize<'a>,
1659 S: AsRef<str> + Sync + Send,
1660 {
1661 let req =
1662 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1663 let res = self.send(req).await?;
1664 self.parse_response(res, false).await
1665 }
1666}
1667
1668#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1669#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1670impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1671
1672fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1674 base: &Url,
1675 request_path: impl RequestPath,
1676 params: Params<'_, K, V>,
1677) -> Url {
1678 let mut url = base.clone();
1679 let mut path_segments = url
1680 .path_segments_mut()
1681 .expect("provided validator url does not have a base!");
1682
1683 path_segments.pop_if_empty();
1684
1685 for segment in request_path.to_sanitized_segments() {
1686 path_segments.push(segment);
1687 }
1688
1689 drop(path_segments);
1692
1693 if !params.is_empty() {
1694 url.query_pairs_mut().extend_pairs(params);
1695 }
1696
1697 url
1698}
1699
1700fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1701 use encoding_rs::{Encoding, UTF_8};
1702
1703 let content_type = try_get_mime_type(headers);
1704
1705 let encoding_name = content_type
1706 .as_ref()
1707 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1708 .unwrap_or("utf-8");
1709
1710 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1711
1712 let (text, _, _) = encoding.decode(bytes);
1713 text.into_owned()
1714}
1715
1716#[instrument(level = "debug", skip_all)]
1718pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1719where
1720 T: DeserializeOwned,
1721{
1722 let status = res.status();
1723 let headers = res.headers().clone();
1724 let url = res.url().clone();
1725
1726 tracing::trace!("status: {status} (success: {})", status.is_success());
1727 tracing::trace!("headers: {headers:?}");
1728
1729 if !allow_empty && let Some(0) = res.content_length() {
1730 return Err(HttpClientError::EmptyResponse {
1731 url: Box::new(url),
1732 status,
1733 headers: Box::new(headers),
1734 });
1735 }
1736
1737 if res.status().is_success() {
1738 let full = res
1741 .bytes()
1742 .await
1743 .map_err(|source| HttpClientError::ResponseReadFailure {
1744 url: Box::new(url),
1745 headers: Box::new(headers.clone()),
1746 status,
1747 source: ReqwestErrorWrapper(source),
1748 })?;
1749 decode_raw_response(&headers, full)
1750 } else if res.status() == StatusCode::NOT_FOUND {
1751 Err(HttpClientError::NotFound { url: Box::new(url) })
1752 } else if is_http_rate_limit_err(&res) {
1753 Err(HttpClientError::EndpointFailure {
1754 url: Box::new(url),
1755 status,
1756 headers: Box::new(headers),
1757 error: String::from("received vercel rate limit challenge response"),
1758 })
1759 } else {
1760 let Ok(plaintext) = res.text().await else {
1761 return Err(HttpClientError::RequestFailure {
1762 url: Box::new(url),
1763 status,
1764 headers: Box::new(headers),
1765 });
1766 };
1767
1768 Err(HttpClientError::EndpointFailure {
1769 url: Box::new(url),
1770 status,
1771 headers: Box::new(headers),
1772 error: plaintext,
1773 })
1774 }
1775}
1776
1777fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1778where
1779 T: DeserializeOwned,
1780{
1781 match serde_json::from_slice(&content) {
1782 Ok(data) => Ok(data),
1783 Err(err) => {
1784 let content = decode_as_text(&content, headers);
1785 Err(HttpClientError::ResponseDecodeFailure {
1786 message: err.to_string(),
1787 content,
1788 })
1789 }
1790 }
1791}
1792
1793fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1794where
1795 T: DeserializeOwned,
1796{
1797 use bincode::Options;
1798
1799 let opts = nym_http_api_common::make_bincode_serializer();
1800 match opts.deserialize(&content) {
1801 Ok(data) => Ok(data),
1802 Err(err) => {
1803 let content = decode_as_text(&content, headers);
1804 Err(HttpClientError::ResponseDecodeFailure {
1805 message: err.to_string(),
1806 content,
1807 })
1808 }
1809 }
1810}
1811
1812fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1813where
1814 T: DeserializeOwned,
1815{
1816 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
1818
1819 debug!("attempting to parse response as {mime}");
1820
1821 match (mime.type_(), mime.subtype().as_str()) {
1823 (mime::APPLICATION, "json") => decode_as_json(headers, content),
1824 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
1825 (_, _) => {
1826 debug!("unrecognised mime type {mime}. falling back to json decoding...");
1827 decode_as_json(headers, content)
1828 }
1829 }
1830}
1831
1832fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
1833 headers
1834 .get(CONTENT_TYPE)
1835 .and_then(|value| value.to_str().ok())
1836 .and_then(|value| value.parse::<Mime>().ok())
1837}
1838
1839#[cfg(test)]
1840mod tests;