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