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;
146
147pub mod registry;
148
149use crate::path::RequestPath;
150use async_trait::async_trait;
151use bytes::Bytes;
152use cfg_if::cfg_if;
153use http::{
154 HeaderMap,
155 header::{ACCEPT, CONTENT_TYPE},
156};
157use itertools::Itertools;
158use mime::Mime;
159use reqwest::{RequestBuilder, Response, header::HeaderValue};
160use serde::{Deserialize, Serialize, de::DeserializeOwned};
161#[cfg(not(target_arch = "wasm32"))]
162use std::io::ErrorKind;
163use std::{
164 fmt::Display,
165 sync::atomic::{AtomicUsize, Ordering},
166 time::Duration,
167};
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) => {
1177 if is_http_rate_limit_err(&resp) {
1179 warn!("encountered vercel rate limit error for {}", url.as_str());
1180 self.maybe_rotate_hosts(Some(url.clone()));
1182 }
1183
1184 return Ok(resp);
1185 }
1186 Err(err) => {
1187 #[cfg(target_arch = "wasm32")]
1188 let is_network_err = err.is_timeout();
1189 #[cfg(not(target_arch = "wasm32"))]
1190 let is_network_err = might_be_network_interference(&err);
1191
1192 if is_network_err {
1193 self.maybe_rotate_hosts(Some(url.clone()));
1195
1196 #[cfg(feature = "tunneling")]
1197 self.maybe_enable_fronting(("network", url.as_str(), &err));
1198 }
1199
1200 if attempts < self.retry_limit {
1201 attempts += 1;
1202 warn!(
1203 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1204 self.retry_limit
1205 );
1206 continue;
1207 }
1208
1209 cfg_if::cfg_if! {
1211 if #[cfg(target_arch = "wasm32")] {
1212 return Err(err);
1213 } else {
1214 return Err(HttpClientError::request_send_error(url.into(), err));
1215 }
1216 }
1217 }
1218 }
1219 }
1220 }
1221
1222 fn maybe_rotate_hosts(&self, offending: Option<Url>) {
1223 self.update_host(offending);
1224 }
1225
1226 #[cfg(feature = "tunneling")]
1227 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
1228 let was_enabled = self.front.is_enabled();
1231 self.front.retry_enable();
1232 if !was_enabled && self.front.is_enabled() {
1233 tracing::debug!("Domain fronting activated after failure: {context:?}",);
1234 }
1235 }
1236}
1237
1238const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
1239const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
1240
1241pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
1243 let status = resp.status() == StatusCode::FORBIDDEN;
1244 let header = resp
1245 .headers()
1246 .get(VERCEL_CHALLENGE_HEADER)
1247 .is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
1248 let content_type = resp
1249 .headers()
1250 .get(CONTENT_TYPE)
1251 .and_then(|value| value.to_str().ok())
1252 .and_then(|value| value.parse::<Mime>().ok())
1253 .is_some_and(|mime_type| {
1254 mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
1255 });
1256
1257 status && header && content_type
1258}
1259
1260#[cfg(not(target_arch = "wasm32"))]
1261const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
1262
1263#[cfg(not(target_arch = "wasm32"))]
1272pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
1273 if err.is_timeout() {
1274 return true;
1275 }
1276
1277 if !(err.is_connect() || err.is_request()) {
1278 return false;
1279 }
1280
1281 let mut inner = err.source();
1286 for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
1287 if let Some(e) = inner {
1288 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
1289 match io_err.kind() {
1291 ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
1293 ErrorKind::ConnectionReset
1295 | ErrorKind::HostUnreachable
1296 | ErrorKind::ConnectionRefused => return true,
1297 ErrorKind::Other | ErrorKind::InvalidData => {
1299 inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
1302 }
1303 _ => return false,
1304 }
1305 } else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
1306 return true;
1308 } else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
1309 return resolve_err.is_nx_domain();
1311 } else {
1312 inner = e.source();
1313 }
1314 } else {
1315 break;
1316 }
1317 }
1318
1319 false
1320}
1321
1322#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1326#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1327pub trait ApiClient: ApiClientCore {
1328 fn create_get_request<P, K, V>(
1330 &self,
1331 path: P,
1332 params: Params<'_, K, V>,
1333 ) -> Result<RequestBuilder, HttpClientError>
1334 where
1335 P: RequestPath,
1336 K: AsRef<str>,
1337 V: AsRef<str>,
1338 {
1339 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1340 }
1341
1342 fn create_post_request<P, B, K, V>(
1344 &self,
1345 path: P,
1346 params: Params<'_, K, V>,
1347 json_body: &B,
1348 ) -> Result<RequestBuilder, HttpClientError>
1349 where
1350 P: RequestPath,
1351 B: Serialize + ?Sized,
1352 K: AsRef<str>,
1353 V: AsRef<str>,
1354 {
1355 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1356 }
1357
1358 fn create_delete_request<P, K, V>(
1360 &self,
1361 path: P,
1362 params: Params<'_, K, V>,
1363 ) -> Result<RequestBuilder, HttpClientError>
1364 where
1365 P: RequestPath,
1366 K: AsRef<str>,
1367 V: AsRef<str>,
1368 {
1369 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1370 }
1371
1372 fn create_patch_request<P, B, K, V>(
1374 &self,
1375 path: P,
1376 params: Params<'_, K, V>,
1377 json_body: &B,
1378 ) -> Result<RequestBuilder, HttpClientError>
1379 where
1380 P: RequestPath,
1381 B: Serialize + ?Sized,
1382 K: AsRef<str>,
1383 V: AsRef<str>,
1384 {
1385 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1386 }
1387
1388 #[instrument(level = "debug", skip_all, fields(path=?path))]
1390 async fn send_get_request<P, K, V>(
1391 &self,
1392 path: P,
1393 params: Params<'_, K, V>,
1394 ) -> Result<Response, HttpClientError>
1395 where
1396 P: RequestPath + Send + Sync,
1397 K: AsRef<str> + Sync,
1398 V: AsRef<str> + Sync,
1399 {
1400 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1401 .await
1402 }
1403
1404 async fn send_post_request<P, B, K, V>(
1406 &self,
1407 path: P,
1408 params: Params<'_, K, V>,
1409 json_body: &B,
1410 ) -> Result<Response, HttpClientError>
1411 where
1412 P: RequestPath + Send + Sync,
1413 B: Serialize + ?Sized + Sync,
1414 K: AsRef<str> + Sync,
1415 V: AsRef<str> + Sync,
1416 {
1417 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1418 .await
1419 }
1420
1421 async fn send_delete_request<P, K, V>(
1423 &self,
1424 path: P,
1425 params: Params<'_, K, V>,
1426 ) -> Result<Response, HttpClientError>
1427 where
1428 P: RequestPath + Send + Sync,
1429 K: AsRef<str> + Sync,
1430 V: AsRef<str> + Sync,
1431 {
1432 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1433 .await
1434 }
1435
1436 async fn send_patch_request<P, B, K, V>(
1438 &self,
1439 path: P,
1440 params: Params<'_, K, V>,
1441 json_body: &B,
1442 ) -> Result<Response, HttpClientError>
1443 where
1444 P: RequestPath + Send + Sync,
1445 B: Serialize + ?Sized + Sync,
1446 K: AsRef<str> + Sync,
1447 V: AsRef<str> + Sync,
1448 {
1449 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1450 .await
1451 }
1452
1453 #[instrument(level = "debug", skip_all, fields(path=?path))]
1457 async fn get_json<P, T, K, V>(
1459 &self,
1460 path: P,
1461 params: Params<'_, K, V>,
1462 ) -> Result<T, HttpClientError>
1463 where
1464 P: RequestPath + Send + Sync,
1465 for<'a> T: Deserialize<'a>,
1466 K: AsRef<str> + Sync,
1467 V: AsRef<str> + Sync,
1468 {
1469 self.get_response(path, params).await
1470 }
1471
1472 async fn parse_response<T>(
1474 &self,
1475 res: Response,
1476 allow_empty: bool,
1477 ) -> Result<T, HttpClientError>
1478 where
1479 T: DeserializeOwned,
1480 {
1481 let url = Url::from(res.url());
1482 parse_response(res, allow_empty).await.inspect_err(|e| {
1483 if matches!(
1484 e,
1487 HttpClientError::ResponseReadFailure {
1488 url: _,
1489 headers: _,
1490 status: _,
1491 source: _,
1492 }
1493 ) {
1494 self.maybe_rotate_hosts(Some(url.clone()));
1495 #[cfg(feature = "tunneling")]
1496 self.maybe_enable_fronting(("parse/read", url.as_str(), e));
1497 }
1498 })
1499 }
1500
1501 #[instrument(level = "debug", skip_all, fields(path=?path))]
1505 async fn get_response<P, T, K, V>(
1506 &self,
1507 path: P,
1508 params: Params<'_, K, V>,
1509 ) -> Result<T, HttpClientError>
1510 where
1511 P: RequestPath + Send + Sync,
1512 for<'a> T: Deserialize<'a>,
1513 K: AsRef<str> + Sync,
1514 V: AsRef<str> + Sync,
1515 {
1516 let res = self
1517 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1518 .await?;
1519
1520 self.parse_response(res, false).await
1521 }
1522
1523 async fn post_json<P, B, T, K, V>(
1527 &self,
1528 path: P,
1529 params: Params<'_, K, V>,
1530 json_body: &B,
1531 ) -> Result<T, HttpClientError>
1532 where
1533 P: RequestPath + Send + Sync,
1534 B: Serialize + ?Sized + Sync,
1535 for<'a> T: Deserialize<'a>,
1536 K: AsRef<str> + Sync,
1537 V: AsRef<str> + Sync,
1538 {
1539 let res = self
1540 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1541 .await?;
1542 self.parse_response(res, false).await
1543 }
1544
1545 async fn delete_json<P, T, K, V>(
1549 &self,
1550 path: P,
1551 params: Params<'_, K, V>,
1552 ) -> Result<T, HttpClientError>
1553 where
1554 P: RequestPath + Send + Sync,
1555 for<'a> T: Deserialize<'a>,
1556 K: AsRef<str> + Sync,
1557 V: AsRef<str> + Sync,
1558 {
1559 let res = self
1560 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1561 .await?;
1562 self.parse_response(res, false).await
1563 }
1564
1565 async fn patch_json<P, B, T, K, V>(
1569 &self,
1570 path: P,
1571 params: Params<'_, K, V>,
1572 json_body: &B,
1573 ) -> Result<T, HttpClientError>
1574 where
1575 P: RequestPath + Send + Sync,
1576 B: Serialize + ?Sized + Sync,
1577 for<'a> T: Deserialize<'a>,
1578 K: AsRef<str> + Sync,
1579 V: AsRef<str> + Sync,
1580 {
1581 let res = self
1582 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1583 .await?;
1584 self.parse_response(res, false).await
1585 }
1586
1587 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1590 where
1591 for<'a> T: Deserialize<'a>,
1592 S: AsRef<str> + Sync + Send,
1593 {
1594 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1595 let res = self.send(req).await?;
1596 self.parse_response(res, false).await
1597 }
1598
1599 async fn post_json_data_to<B, T, S>(
1602 &self,
1603 endpoint: S,
1604 json_body: &B,
1605 ) -> Result<T, HttpClientError>
1606 where
1607 B: Serialize + ?Sized + Sync,
1608 for<'a> T: Deserialize<'a>,
1609 S: AsRef<str> + Sync + Send,
1610 {
1611 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1612 let res = self.send(req).await?;
1613 self.parse_response(res, false).await
1614 }
1615
1616 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1619 where
1620 for<'a> T: Deserialize<'a>,
1621 S: AsRef<str> + Sync + Send,
1622 {
1623 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1624 let res = self.send(req).await?;
1625 self.parse_response(res, false).await
1626 }
1627
1628 async fn patch_json_data_at<B, T, S>(
1631 &self,
1632 endpoint: S,
1633 json_body: &B,
1634 ) -> Result<T, HttpClientError>
1635 where
1636 B: Serialize + ?Sized + Sync,
1637 for<'a> T: Deserialize<'a>,
1638 S: AsRef<str> + Sync + Send,
1639 {
1640 let req =
1641 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1642 let res = self.send(req).await?;
1643 self.parse_response(res, false).await
1644 }
1645}
1646
1647#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1648#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1649impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1650
1651fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1653 base: &Url,
1654 request_path: impl RequestPath,
1655 params: Params<'_, K, V>,
1656) -> Url {
1657 let mut url = base.clone();
1658 let mut path_segments = url
1659 .path_segments_mut()
1660 .expect("provided validator url does not have a base!");
1661
1662 path_segments.pop_if_empty();
1663
1664 for segment in request_path.to_sanitized_segments() {
1665 path_segments.push(segment);
1666 }
1667
1668 drop(path_segments);
1671
1672 if !params.is_empty() {
1673 url.query_pairs_mut().extend_pairs(params);
1674 }
1675
1676 url
1677}
1678
1679fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1680 use encoding_rs::{Encoding, UTF_8};
1681
1682 let content_type = try_get_mime_type(headers);
1683
1684 let encoding_name = content_type
1685 .as_ref()
1686 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1687 .unwrap_or("utf-8");
1688
1689 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1690
1691 let (text, _, _) = encoding.decode(bytes);
1692 text.into_owned()
1693}
1694
1695#[instrument(level = "debug", skip_all)]
1697pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1698where
1699 T: DeserializeOwned,
1700{
1701 let status = res.status();
1702 let headers = res.headers().clone();
1703 let url = res.url().clone();
1704
1705 tracing::trace!("status: {status} (success: {})", status.is_success());
1706 tracing::trace!("headers: {headers:?}");
1707
1708 if !allow_empty && let Some(0) = res.content_length() {
1709 return Err(HttpClientError::EmptyResponse {
1710 url: Box::new(url),
1711 status,
1712 headers: Box::new(headers),
1713 });
1714 }
1715
1716 if res.status().is_success() {
1717 let full = res
1720 .bytes()
1721 .await
1722 .map_err(|source| HttpClientError::ResponseReadFailure {
1723 url: Box::new(url),
1724 headers: Box::new(headers.clone()),
1725 status,
1726 source: ReqwestErrorWrapper(source),
1727 })?;
1728 decode_raw_response(&headers, full)
1729 } else if res.status() == StatusCode::NOT_FOUND {
1730 Err(HttpClientError::NotFound { url: Box::new(url) })
1731 } else if is_http_rate_limit_err(&res) {
1732 Err(HttpClientError::EndpointFailure {
1733 url: Box::new(url),
1734 status,
1735 headers: Box::new(headers),
1736 error: String::from("received vercel rate limit challenge response"),
1737 })
1738 } else {
1739 let Ok(plaintext) = res.text().await else {
1740 return Err(HttpClientError::RequestFailure {
1741 url: Box::new(url),
1742 status,
1743 headers: Box::new(headers),
1744 });
1745 };
1746
1747 Err(HttpClientError::EndpointFailure {
1748 url: Box::new(url),
1749 status,
1750 headers: Box::new(headers),
1751 error: plaintext,
1752 })
1753 }
1754}
1755
1756fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1757where
1758 T: DeserializeOwned,
1759{
1760 match serde_json::from_slice(&content) {
1761 Ok(data) => Ok(data),
1762 Err(err) => {
1763 let content = decode_as_text(&content, headers);
1764 Err(HttpClientError::ResponseDecodeFailure {
1765 message: err.to_string(),
1766 content,
1767 })
1768 }
1769 }
1770}
1771
1772fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1773where
1774 T: DeserializeOwned,
1775{
1776 use bincode::Options;
1777
1778 let opts = nym_http_api_common::make_bincode_serializer();
1779 match opts.deserialize(&content) {
1780 Ok(data) => Ok(data),
1781 Err(err) => {
1782 let content = decode_as_text(&content, headers);
1783 Err(HttpClientError::ResponseDecodeFailure {
1784 message: err.to_string(),
1785 content,
1786 })
1787 }
1788 }
1789}
1790
1791fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1792where
1793 T: DeserializeOwned,
1794{
1795 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
1797
1798 debug!("attempting to parse response as {mime}");
1799
1800 match (mime.type_(), mime.subtype().as_str()) {
1802 (mime::APPLICATION, "json") => decode_as_json(headers, content),
1803 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
1804 (_, _) => {
1805 debug!("unrecognised mime type {mime}. falling back to json decoding...");
1806 decode_as_json(headers, content)
1807 }
1808 }
1809}
1810
1811fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
1812 headers
1813 .get(CONTENT_TYPE)
1814 .and_then(|value| value.to_str().ok())
1815 .and_then(|value| value.parse::<Mime>().ok())
1816}
1817
1818#[cfg(test)]
1819mod tests;