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