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();
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() && self.current_url().has_front() {
965 url.host_str() == self.current_url().front_str()
966 } else {
967 url.host_str() == self.current_url().host_str()
968 }
969 }
970
971 #[cfg(not(feature = "tunneling"))]
972 fn matches_current_host(&self, url: &Url) -> bool {
973 url.host_str() == self.current_url().host_str()
974 }
975
976 fn update_host(&self, maybe_url: Option<Url>) {
983 if let Some(err_url) = maybe_url
985 && !self.matches_current_host(&err_url)
986 {
987 return;
988 }
989
990 #[cfg(feature = "tunneling")]
991 if self.front.is_enabled() {
992 let url = self.current_url();
994
995 if url.has_front() && !url.update() {
998 return;
1000 }
1001 }
1002
1003 if self.base_urls.len() > 1 {
1004 let orig = self.current_idx.load(Ordering::Relaxed);
1005
1006 #[allow(unused_mut)]
1007 let mut next = (orig + 1) % self.base_urls.len();
1008
1009 #[cfg(feature = "tunneling")]
1011 if self.front.is_enabled() {
1012 while next != orig {
1013 if self.base_urls[next].has_front() {
1014 break;
1016 }
1017
1018 next = (next + 1) % self.base_urls.len();
1019 }
1020 }
1021
1022 self.current_idx.store(next, Ordering::Relaxed);
1023 debug!(
1024 "http client rotating host {} -> {}",
1025 self.base_urls[orig], self.base_urls[next]
1026 );
1027 }
1028 }
1029
1030 pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
1043 let url = self.current_url();
1044 r.url_mut().set_host(url.host_str()).unwrap();
1045
1046 #[cfg(feature = "tunneling")]
1047 if self.front.is_enabled() {
1048 if let Some(front_host) = url.front_str() {
1049 if let Some(actual_host) = url.host_str() {
1050 tracing::debug!(
1051 "Domain fronting enabled: routing via CDN {} to actual host {}",
1052 front_host,
1053 actual_host
1054 );
1055
1056 r.url_mut().set_host(Some(front_host)).unwrap();
1058
1059 let actual_host_header: HeaderValue =
1060 actual_host.parse().unwrap_or(HeaderValue::from_static(""));
1061 _ = r
1064 .headers_mut()
1065 .insert(reqwest::header::HOST, actual_host_header);
1066
1067 let front_host_header: HeaderValue =
1069 front_host.parse().unwrap_or(HeaderValue::from_static(""));
1070 _ = r
1071 .headers_mut()
1072 .insert(NYM_OUTER_SNI_HEADER, front_host_header);
1073
1074 return (url.as_str(), url.front_str());
1075 } else {
1076 tracing::debug!(
1077 "Domain fronting is enabled, but no host_url is defined for current URL"
1078 )
1079 }
1080 } else {
1081 tracing::debug!(
1082 "Domain fronting is enabled, but current URL has no front_hosts configured"
1083 )
1084 }
1085 }
1086 (url.as_str(), None)
1087 }
1088}
1089
1090#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1091#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1092impl ApiClientCore for Client {
1093 #[instrument(level = "debug", skip_all, fields(path=?path))]
1094 fn create_request<P, B, K, V>(
1095 &self,
1096 method: reqwest::Method,
1097 path: P,
1098 params: Params<'_, K, V>,
1099 body: Option<&B>,
1100 ) -> Result<RequestBuilder, HttpClientError>
1101 where
1102 P: RequestPath,
1103 B: Serialize + ?Sized,
1104 K: AsRef<str>,
1105 V: AsRef<str>,
1106 {
1107 let url = self.current_url();
1108 let url = sanitize_url(url, path, params);
1109
1110 let mut req = reqwest::Request::new(method, url.into());
1111
1112 self.apply_hosts_to_req(&mut req);
1113
1114 let client = if let Some(client) = &self.reqwest_client {
1115 client.clone()
1116 } else {
1117 SHARED_CLIENT.clone()
1118 };
1119 let mut rb = RequestBuilder::from_parts(client, req);
1120
1121 rb = rb
1122 .header(ACCEPT, self.serialization.content_type())
1123 .header(CONTENT_TYPE, self.serialization.content_type());
1124
1125 if let Some(user_agent) = &self.custom_user_agent {
1126 rb = rb.header(USER_AGENT, user_agent.clone());
1127 }
1128
1129 if let Some(body) = body {
1130 match self.serialization {
1131 SerializationFormat::Json => {
1132 rb = rb.json(body);
1133 }
1134 SerializationFormat::Bincode => {
1135 let body = bincode::serialize(body)?;
1136 rb = rb.body(body);
1137 }
1138 SerializationFormat::Yaml => {
1139 let mut body_bytes = Vec::new();
1140 serde_yaml::to_writer(&mut body_bytes, &body)?;
1141 rb = rb.body(body_bytes);
1142 }
1143 SerializationFormat::Text => {
1144 let body = serde_plain::to_string(&body)?.as_bytes().to_vec();
1145 rb = rb.body(body);
1146 }
1147 }
1148 }
1149
1150 Ok(rb)
1151 }
1152
1153 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError> {
1154 let mut attempts = 0;
1155 loop {
1156 let r = request
1158 .try_clone()
1159 .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?;
1160
1161 let mut req = r
1164 .build()
1165 .map_err(HttpClientError::reqwest_client_build_error)?;
1166 self.apply_hosts_to_req(&mut req);
1167 let url: Url = req.url().clone().into();
1168
1169 let request_start = Instant::now();
1170
1171 #[cfg(target_arch = "wasm32")]
1172 let response: Result<Response, HttpClientError> = {
1173 let client = self
1174 .reqwest_client
1175 .as_ref()
1176 .unwrap_or_else(|| &*SHARED_CLIENT);
1177 Ok(
1178 wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
1179 .await
1180 .map_err(|_timeout| HttpClientError::RequestTimeout)??,
1181 )
1182 };
1183
1184 #[cfg(not(target_arch = "wasm32"))]
1185 let response = {
1186 let client = self
1187 .reqwest_client
1188 .as_ref()
1189 .unwrap_or_else(|| &*SHARED_CLIENT);
1190 client.execute(req).await
1191 };
1192
1193 match response {
1194 Ok(resp) => {
1195 if is_http_rate_limit_err(&resp) {
1197 warn!("encountered vercel rate limit error for {}", url.as_str());
1198 self.maybe_rotate_hosts(Some(url.clone()));
1200 }
1201
1202 return Ok(resp);
1203 }
1204 Err(err) => {
1205 let last_network_reconfiguration =
1206 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap();
1207 let network_reconfigured =
1208 last_network_reconfiguration.is_some_and(|last| last > request_start);
1209
1210 #[cfg(target_arch = "wasm32")]
1211 let is_network_err = err.is_timeout();
1212 #[cfg(not(target_arch = "wasm32"))]
1213 let is_network_err = might_be_network_interference(&err);
1214
1215 if is_network_err & !network_reconfigured {
1216 self.maybe_rotate_hosts(Some(url.clone()));
1218
1219 #[cfg(feature = "tunneling")]
1220 self.maybe_enable_fronting(("network", url.as_str(), &err));
1221 }
1222
1223 if attempts < self.retry_limit {
1224 attempts += 1;
1225 warn!(
1226 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1227 self.retry_limit
1228 );
1229 continue;
1230 }
1231
1232 cfg_if::cfg_if! {
1234 if #[cfg(target_arch = "wasm32")] {
1235 return Err(err);
1236 } else {
1237 return Err(HttpClientError::request_send_error(url.into(), err));
1238 }
1239 }
1240 }
1241 }
1242 }
1243 }
1244
1245 fn maybe_rotate_hosts(&self, offending: Option<Url>) {
1246 self.update_host(offending);
1247 }
1248
1249 #[cfg(feature = "tunneling")]
1250 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
1251 let was_enabled = self.front.is_enabled();
1254 self.front.retry_enable();
1255 if !was_enabled && self.front.is_enabled() {
1256 tracing::debug!("Domain fronting activated after failure: {context:?}",);
1257 }
1258 }
1259}
1260
1261const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
1262const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
1263
1264pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
1266 let status = resp.status() == StatusCode::FORBIDDEN;
1267 let header = resp
1268 .headers()
1269 .get(VERCEL_CHALLENGE_HEADER)
1270 .is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
1271 let content_type = resp
1272 .headers()
1273 .get(CONTENT_TYPE)
1274 .and_then(|value| value.to_str().ok())
1275 .and_then(|value| value.parse::<Mime>().ok())
1276 .is_some_and(|mime_type| {
1277 mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
1278 });
1279
1280 status && header && content_type
1281}
1282
1283#[cfg(not(target_arch = "wasm32"))]
1284const MAX_ERR_SOURCE_ITERATIONS: usize = 6;
1285
1286#[cfg(not(target_arch = "wasm32"))]
1295pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
1296 if err.is_timeout() {
1297 return true;
1298 }
1299
1300 if !(err.is_connect() || err.is_request()) {
1301 return false;
1302 }
1303
1304 let mut inner = err.source();
1309 for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
1310 if let Some(e) = inner {
1311 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
1312 match io_err.kind() {
1314 ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
1316 ErrorKind::ConnectionReset
1318 | ErrorKind::HostUnreachable
1319 | ErrorKind::ConnectionRefused => return true,
1320 ErrorKind::Other | ErrorKind::InvalidData => {
1322 inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
1325 }
1326 _ => return false,
1327 }
1328 } else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
1329 return true;
1331 } else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
1332 return resolve_err.is_nx_domain();
1334 } else if let Some(h2_err) = e.downcast_ref::<h2::Error>() {
1335 return h2_err.is_remote() && (h2_err.is_reset() || h2_err.is_go_away());
1344 } else {
1345 inner = e.source();
1346 }
1347 } else {
1348 break;
1349 }
1350 }
1351
1352 false
1353}
1354
1355#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1359#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1360pub trait ApiClient: ApiClientCore {
1361 fn create_get_request<P, K, V>(
1363 &self,
1364 path: P,
1365 params: Params<'_, K, V>,
1366 ) -> Result<RequestBuilder, HttpClientError>
1367 where
1368 P: RequestPath,
1369 K: AsRef<str>,
1370 V: AsRef<str>,
1371 {
1372 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1373 }
1374
1375 fn create_post_request<P, B, K, V>(
1377 &self,
1378 path: P,
1379 params: Params<'_, K, V>,
1380 json_body: &B,
1381 ) -> Result<RequestBuilder, HttpClientError>
1382 where
1383 P: RequestPath,
1384 B: Serialize + ?Sized,
1385 K: AsRef<str>,
1386 V: AsRef<str>,
1387 {
1388 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1389 }
1390
1391 fn create_delete_request<P, K, V>(
1393 &self,
1394 path: P,
1395 params: Params<'_, K, V>,
1396 ) -> Result<RequestBuilder, HttpClientError>
1397 where
1398 P: RequestPath,
1399 K: AsRef<str>,
1400 V: AsRef<str>,
1401 {
1402 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1403 }
1404
1405 fn create_patch_request<P, B, K, V>(
1407 &self,
1408 path: P,
1409 params: Params<'_, K, V>,
1410 json_body: &B,
1411 ) -> Result<RequestBuilder, HttpClientError>
1412 where
1413 P: RequestPath,
1414 B: Serialize + ?Sized,
1415 K: AsRef<str>,
1416 V: AsRef<str>,
1417 {
1418 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1419 }
1420
1421 #[instrument(level = "debug", skip_all, fields(path=?path))]
1423 async fn send_get_request<P, K, V>(
1424 &self,
1425 path: P,
1426 params: Params<'_, K, V>,
1427 ) -> Result<Response, HttpClientError>
1428 where
1429 P: RequestPath + Send + Sync,
1430 K: AsRef<str> + Sync,
1431 V: AsRef<str> + Sync,
1432 {
1433 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1434 .await
1435 }
1436
1437 async fn send_post_request<P, B, K, V>(
1439 &self,
1440 path: P,
1441 params: Params<'_, K, V>,
1442 json_body: &B,
1443 ) -> Result<Response, HttpClientError>
1444 where
1445 P: RequestPath + Send + Sync,
1446 B: Serialize + ?Sized + Sync,
1447 K: AsRef<str> + Sync,
1448 V: AsRef<str> + Sync,
1449 {
1450 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1451 .await
1452 }
1453
1454 async fn send_delete_request<P, K, V>(
1456 &self,
1457 path: P,
1458 params: Params<'_, K, V>,
1459 ) -> Result<Response, HttpClientError>
1460 where
1461 P: RequestPath + Send + Sync,
1462 K: AsRef<str> + Sync,
1463 V: AsRef<str> + Sync,
1464 {
1465 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1466 .await
1467 }
1468
1469 async fn send_patch_request<P, B, K, V>(
1471 &self,
1472 path: P,
1473 params: Params<'_, K, V>,
1474 json_body: &B,
1475 ) -> Result<Response, HttpClientError>
1476 where
1477 P: RequestPath + Send + Sync,
1478 B: Serialize + ?Sized + Sync,
1479 K: AsRef<str> + Sync,
1480 V: AsRef<str> + Sync,
1481 {
1482 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1483 .await
1484 }
1485
1486 #[instrument(level = "debug", skip_all, fields(path=?path))]
1490 async fn get_json<P, T, K, V>(
1492 &self,
1493 path: P,
1494 params: Params<'_, K, V>,
1495 ) -> Result<T, HttpClientError>
1496 where
1497 P: RequestPath + Send + Sync,
1498 for<'a> T: Deserialize<'a>,
1499 K: AsRef<str> + Sync,
1500 V: AsRef<str> + Sync,
1501 {
1502 self.get_response(path, params).await
1503 }
1504
1505 async fn parse_response<T>(
1507 &self,
1508 res: Response,
1509 allow_empty: bool,
1510 ) -> Result<T, HttpClientError>
1511 where
1512 T: DeserializeOwned,
1513 {
1514 let url = Url::from(res.url());
1515 parse_response(res, allow_empty).await.inspect_err(|e| {
1516 if matches!(
1517 e,
1520 HttpClientError::ResponseReadFailure {
1521 url: _,
1522 headers: _,
1523 status: _,
1524 source: _,
1525 }
1526 ) {
1527 self.maybe_rotate_hosts(Some(url.clone()));
1528 #[cfg(feature = "tunneling")]
1529 self.maybe_enable_fronting(("parse/read", url.as_str(), e));
1530 }
1531 })
1532 }
1533
1534 #[instrument(level = "debug", skip_all, fields(path=?path))]
1538 async fn get_response<P, T, K, V>(
1539 &self,
1540 path: P,
1541 params: Params<'_, K, V>,
1542 ) -> Result<T, HttpClientError>
1543 where
1544 P: RequestPath + Send + Sync,
1545 for<'a> T: Deserialize<'a>,
1546 K: AsRef<str> + Sync,
1547 V: AsRef<str> + Sync,
1548 {
1549 let res = self
1550 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1551 .await?;
1552
1553 self.parse_response(res, false).await
1554 }
1555
1556 async fn post_json<P, B, T, K, V>(
1560 &self,
1561 path: P,
1562 params: Params<'_, K, V>,
1563 json_body: &B,
1564 ) -> Result<T, HttpClientError>
1565 where
1566 P: RequestPath + Send + Sync,
1567 B: Serialize + ?Sized + Sync,
1568 for<'a> T: Deserialize<'a>,
1569 K: AsRef<str> + Sync,
1570 V: AsRef<str> + Sync,
1571 {
1572 let res = self
1573 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1574 .await?;
1575 self.parse_response(res, false).await
1576 }
1577
1578 async fn delete_json<P, T, K, V>(
1582 &self,
1583 path: P,
1584 params: Params<'_, K, V>,
1585 ) -> Result<T, HttpClientError>
1586 where
1587 P: RequestPath + Send + Sync,
1588 for<'a> T: Deserialize<'a>,
1589 K: AsRef<str> + Sync,
1590 V: AsRef<str> + Sync,
1591 {
1592 let res = self
1593 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1594 .await?;
1595 self.parse_response(res, false).await
1596 }
1597
1598 async fn patch_json<P, B, T, K, V>(
1602 &self,
1603 path: P,
1604 params: Params<'_, K, V>,
1605 json_body: &B,
1606 ) -> Result<T, HttpClientError>
1607 where
1608 P: RequestPath + Send + Sync,
1609 B: Serialize + ?Sized + Sync,
1610 for<'a> T: Deserialize<'a>,
1611 K: AsRef<str> + Sync,
1612 V: AsRef<str> + Sync,
1613 {
1614 let res = self
1615 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1616 .await?;
1617 self.parse_response(res, false).await
1618 }
1619
1620 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1623 where
1624 for<'a> T: Deserialize<'a>,
1625 S: AsRef<str> + Sync + Send,
1626 {
1627 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1628 let res = self.send(req).await?;
1629 self.parse_response(res, false).await
1630 }
1631
1632 async fn post_json_data_to<B, T, S>(
1635 &self,
1636 endpoint: S,
1637 json_body: &B,
1638 ) -> Result<T, HttpClientError>
1639 where
1640 B: Serialize + ?Sized + Sync,
1641 for<'a> T: Deserialize<'a>,
1642 S: AsRef<str> + Sync + Send,
1643 {
1644 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1645 let res = self.send(req).await?;
1646 self.parse_response(res, false).await
1647 }
1648
1649 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1652 where
1653 for<'a> T: Deserialize<'a>,
1654 S: AsRef<str> + Sync + Send,
1655 {
1656 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1657 let res = self.send(req).await?;
1658 self.parse_response(res, false).await
1659 }
1660
1661 async fn patch_json_data_at<B, T, S>(
1664 &self,
1665 endpoint: S,
1666 json_body: &B,
1667 ) -> Result<T, HttpClientError>
1668 where
1669 B: Serialize + ?Sized + Sync,
1670 for<'a> T: Deserialize<'a>,
1671 S: AsRef<str> + Sync + Send,
1672 {
1673 let req =
1674 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1675 let res = self.send(req).await?;
1676 self.parse_response(res, false).await
1677 }
1678}
1679
1680#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1681#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1682impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1683
1684fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1686 base: &Url,
1687 request_path: impl RequestPath,
1688 params: Params<'_, K, V>,
1689) -> Url {
1690 let mut url = base.clone();
1691 let mut path_segments = url
1692 .path_segments_mut()
1693 .expect("provided validator url does not have a base!");
1694
1695 path_segments.pop_if_empty();
1696
1697 for segment in request_path.to_sanitized_segments() {
1698 path_segments.push(segment);
1699 }
1700
1701 drop(path_segments);
1704
1705 if !params.is_empty() {
1706 url.query_pairs_mut().extend_pairs(params);
1707 }
1708
1709 url
1710}
1711
1712fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1713 use encoding_rs::{Encoding, UTF_8};
1714
1715 let content_type = try_get_mime_type(headers);
1716
1717 let encoding_name = content_type
1718 .as_ref()
1719 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1720 .unwrap_or("utf-8");
1721
1722 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1723
1724 let (text, _, _) = encoding.decode(bytes);
1725 text.into_owned()
1726}
1727
1728#[instrument(level = "debug", skip_all)]
1730pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1731where
1732 T: DeserializeOwned,
1733{
1734 let status = res.status();
1735 let headers = res.headers().clone();
1736 let url = res.url().clone();
1737
1738 tracing::trace!("status: {status} (success: {})", status.is_success());
1739 tracing::trace!("headers: {headers:?}");
1740
1741 if !allow_empty && let Some(0) = res.content_length() {
1742 return Err(HttpClientError::EmptyResponse {
1743 url: Box::new(url),
1744 status,
1745 headers: Box::new(headers),
1746 });
1747 }
1748
1749 if res.status().is_success() {
1750 let full = res
1753 .bytes()
1754 .await
1755 .map_err(|source| HttpClientError::ResponseReadFailure {
1756 url: Box::new(url),
1757 headers: Box::new(headers.clone()),
1758 status,
1759 source: ReqwestErrorWrapper(source),
1760 })?;
1761 decode_raw_response(&headers, full)
1762 } else if res.status() == StatusCode::NOT_FOUND {
1763 Err(HttpClientError::NotFound { url: Box::new(url) })
1764 } else if is_http_rate_limit_err(&res) {
1765 Err(HttpClientError::EndpointFailure {
1766 url: Box::new(url),
1767 status,
1768 headers: Box::new(headers),
1769 error: String::from("received vercel rate limit challenge response"),
1770 })
1771 } else {
1772 let Ok(plaintext) = res.text().await else {
1773 return Err(HttpClientError::RequestFailure {
1774 url: Box::new(url),
1775 status,
1776 headers: Box::new(headers),
1777 });
1778 };
1779
1780 Err(HttpClientError::EndpointFailure {
1781 url: Box::new(url),
1782 status,
1783 headers: Box::new(headers),
1784 error: plaintext,
1785 })
1786 }
1787}
1788
1789fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1790where
1791 T: DeserializeOwned,
1792{
1793 match serde_json::from_slice(&content) {
1794 Ok(data) => Ok(data),
1795 Err(err) => {
1796 let content = decode_as_text(&content, headers);
1797 Err(HttpClientError::ResponseDecodeFailure {
1798 message: err.to_string(),
1799 content,
1800 })
1801 }
1802 }
1803}
1804
1805fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1806where
1807 T: DeserializeOwned,
1808{
1809 use bincode::Options;
1810
1811 let opts = nym_http_api_common::make_bincode_serializer();
1812 match opts.deserialize(&content) {
1813 Ok(data) => Ok(data),
1814 Err(err) => {
1815 let content = decode_as_text(&content, headers);
1816 Err(HttpClientError::ResponseDecodeFailure {
1817 message: err.to_string(),
1818 content,
1819 })
1820 }
1821 }
1822}
1823
1824fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1825where
1826 T: DeserializeOwned,
1827{
1828 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
1830
1831 debug!("attempting to parse response as {mime}");
1832
1833 match (mime.type_(), mime.subtype().as_str()) {
1835 (mime::APPLICATION, "json") => decode_as_json(headers, content),
1836 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
1837 (_, _) => {
1838 debug!("unrecognised mime type {mime}. falling back to json decoding...");
1839 decode_as_json(headers, content)
1840 }
1841 }
1842}
1843
1844fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
1845 headers
1846 .get(CONTENT_TYPE)
1847 .and_then(|value| value.to_str().ok())
1848 .and_then(|value| value.parse::<Mime>().ok())
1849}
1850
1851#[cfg(test)]
1852mod tests;