1#![warn(missing_docs)]
138
139pub use inventory;
140pub use reqwest;
141pub use reqwest::ClientBuilder as ReqwestClientBuilder;
142pub use reqwest::StatusCode;
143use std::error::Error;
144
145pub mod registry;
146
147use crate::path::RequestPath;
148use async_trait::async_trait;
149use bytes::Bytes;
150use http::HeaderMap;
151use http::header::{ACCEPT, CONTENT_TYPE};
152use itertools::Itertools;
153use mime::Mime;
154use reqwest::header::HeaderValue;
155use reqwest::{RequestBuilder, Response};
156use serde::de::DeserializeOwned;
157use serde::{Deserialize, Serialize};
158use std::fmt::Display;
159use std::sync::atomic::{AtomicUsize, Ordering};
160use std::time::Duration;
161use thiserror::Error;
162use tracing::{debug, instrument, warn};
163
164#[cfg(not(target_arch = "wasm32"))]
165use std::net::SocketAddr;
166use std::sync::Arc;
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
198#[cfg(not(target_arch = "wasm32"))]
199client_defaults!(
200 priority = -100;
201 gzip = true,
202 deflate = true,
203 brotli = true,
204 zstd = true,
205 timeout = DEFAULT_TIMEOUT,
206 user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
207);
208
209pub type PathSegments<'a> = &'a [&'a str];
211pub type Params<'a, K, V> = &'a [(K, V)];
213
214pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum SerializationFormat {
220 Json,
222 Bincode,
224 Yaml,
226 Text,
228}
229
230impl Display for SerializationFormat {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match self {
233 SerializationFormat::Json => write!(f, "json"),
234 SerializationFormat::Bincode => write!(f, "bincode"),
235 SerializationFormat::Yaml => write!(f, "yaml"),
236 SerializationFormat::Text => write!(f, "text"),
237 }
238 }
239}
240
241impl SerializationFormat {
242 #[allow(missing_docs)]
243 pub fn content_type(&self) -> String {
244 match self {
245 SerializationFormat::Json => "application/json".to_string(),
246 SerializationFormat::Bincode => "application/bincode".to_string(),
247 SerializationFormat::Yaml => "application/yaml".to_string(),
248 SerializationFormat::Text => "text/plain".to_string(),
249 }
250 }
251}
252
253#[allow(missing_docs)]
254#[derive(Debug)]
255pub struct ReqwestErrorWrapper(reqwest::Error);
256
257impl Display for ReqwestErrorWrapper {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 cfg_if::cfg_if! {
260 if #[cfg(not(target_arch = "wasm32"))] {
261 if self.0.is_connect() {
262 write!(f, "failed to connect: ")?;
263 }
264 }
265 }
266
267 if self.0.is_timeout() {
268 write!(f, "timed out: ")?;
269 }
270 if self.0.is_redirect()
271 && let Some(final_stop) = self.0.url()
272 {
273 write!(f, "redirect loop at {final_stop}: ")?;
274 }
275
276 self.0.fmt(f)?;
277 if let Some(status_code) = self.0.status() {
278 write!(f, " status: {status_code}")?;
279 } else {
280 write!(f, " unknown status code")?;
281 }
282
283 if let Some(source) = self.0.source() {
284 write!(f, " source: {source}")?;
285 } else {
286 write!(f, " unknown lower-level error source")?;
287 }
288
289 Ok(())
290 }
291}
292
293impl std::error::Error for ReqwestErrorWrapper {}
294
295#[derive(Debug, Error)]
297#[allow(missing_docs)]
298pub enum HttpClientError {
299 #[error("did not provide any valid client URLs")]
300 NoUrlsProvided,
301
302 #[error("failed to construct inner reqwest client: {source}")]
303 ReqwestBuildError {
304 #[source]
305 source: reqwest::Error,
306 },
307
308 #[deprecated(
309 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
310 )]
311 #[error("request failed with error message: {0}")]
312 GenericRequestFailure(String),
313
314 #[deprecated(
315 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
316 )]
317 #[error("there was an issue with the REST request: {source}")]
318 ReqwestClientError {
319 #[from]
320 source: reqwest::Error,
321 },
322
323 #[error("failed to parse {raw} as a valid URL: {source}")]
324 MalformedUrl {
325 raw: String,
326 #[source]
327 source: reqwest::Error,
328 },
329
330 #[error("failed to send request for {url}: {source}")]
331 RequestSendFailure {
332 url: reqwest::Url,
333 #[source]
334 source: ReqwestErrorWrapper,
335 },
336
337 #[error("failed to read response body from {url}: {source}")]
338 ResponseReadFailure {
339 url: reqwest::Url,
340 headers: Box<HeaderMap>,
341 status: StatusCode,
342 #[source]
343 source: ReqwestErrorWrapper,
344 },
345
346 #[error("failed to deserialize received response: {source}")]
347 ResponseDeserialisationFailure { source: serde_json::Error },
348
349 #[error("provided url is malformed: {source}")]
350 UrlParseFailure {
351 #[from]
352 source: url::ParseError,
353 },
354
355 #[error("the requested resource could not be found at {url}")]
356 NotFound { url: reqwest::Url },
357
358 #[error("attempted to use domain fronting and clone a request containing stream data")]
359 AttemptedToCloneStreamRequest,
360
361 #[error(
365 "the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
366 )]
367 RequestFailure {
368 url: reqwest::Url,
369 status: StatusCode,
370 headers: Box<HeaderMap>,
371 },
372
373 #[error(
374 "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
375 )]
376 EmptyResponse {
377 url: reqwest::Url,
378 status: StatusCode,
379 headers: Box<HeaderMap>,
380 },
381
382 #[error(
383 "failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
384 )]
385 EndpointFailure {
386 url: reqwest::Url,
387 status: StatusCode,
388 headers: Box<HeaderMap>,
389 error: String,
390 },
391
392 #[error("failed to decode response body: {message} from {content}")]
393 ResponseDecodeFailure { message: String, content: String },
394
395 #[error("failed to resolve request to {url} due to data inconsistency: {details}")]
396 InternalResponseInconsistency { url: ::url::Url, details: String },
397
398 #[cfg(not(target_arch = "wasm32"))]
399 #[error("encountered dns failure: {inner}")]
400 DnsLookupFailure {
401 #[from]
402 inner: ResolveError,
403 },
404
405 #[error("Failed to encode bincode: {0}")]
406 Bincode(#[from] bincode::Error),
407
408 #[error("Failed to json: {0}")]
409 Json(#[from] serde_json::Error),
410
411 #[error("Failed to yaml: {0}")]
412 Yaml(#[from] serde_yaml::Error),
413
414 #[error("Failed to plain: {0}")]
415 Plain(#[from] serde_plain::Error),
416
417 #[cfg(target_arch = "wasm32")]
418 #[error("the request has timed out")]
419 RequestTimeout,
420}
421
422#[allow(missing_docs)]
423#[allow(deprecated)]
424impl HttpClientError {
425 pub fn is_timeout(&self) -> bool {
427 match self {
428 HttpClientError::ReqwestClientError { source } => source.is_timeout(),
429 HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
430 HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
431 #[cfg(not(target_arch = "wasm32"))]
432 HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
433 #[cfg(target_arch = "wasm32")]
434 HttpClientError::RequestTimeout => true,
435 _ => false,
436 }
437 }
438
439 pub fn status_code(&self) -> Option<StatusCode> {
441 match self {
442 HttpClientError::ResponseReadFailure { status, .. } => Some(*status),
443 HttpClientError::RequestFailure { status, .. } => Some(*status),
444 HttpClientError::EmptyResponse { status, .. } => Some(*status),
445 HttpClientError::EndpointFailure { status, .. } => Some(*status),
446 _ => None,
447 }
448 }
449
450 pub fn reqwest_client_build_error(source: reqwest::Error) -> Self {
451 HttpClientError::ReqwestBuildError { source }
452 }
453
454 pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
455 HttpClientError::RequestSendFailure {
456 url,
457 source: ReqwestErrorWrapper(source),
458 }
459 }
460}
461
462#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
468#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
469pub trait ApiClientCore {
470 fn create_request<P, B, K, V>(
472 &self,
473 method: reqwest::Method,
474 path: P,
475 params: Params<'_, K, V>,
476 body: Option<&B>,
477 ) -> Result<RequestBuilder, HttpClientError>
478 where
479 P: RequestPath,
480 B: Serialize + ?Sized,
481 K: AsRef<str>,
482 V: AsRef<str>;
483
484 fn create_request_endpoint<B, S>(
500 &self,
501 method: reqwest::Method,
502 endpoint: S,
503 body: Option<&B>,
504 ) -> Result<RequestBuilder, HttpClientError>
505 where
506 B: Serialize + ?Sized,
507 S: AsRef<str>,
508 {
509 let mut standin_url: Url = "http://example.com".parse().unwrap();
514
515 match endpoint.as_ref().split_once("?") {
516 Some((path, query)) => {
517 standin_url.set_path(path);
518 standin_url.set_query(Some(query));
519 }
520 None => standin_url.set_path(endpoint.as_ref()),
522 }
523
524 let path: Vec<&str> = match standin_url.path_segments() {
525 Some(segments) => segments.collect(),
526 None => Vec::new(),
527 };
528 let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
529
530 self.create_request(method, path.as_slice(), ¶ms, body)
531 }
532
533 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError>;
539
540 async fn send_request<P, B, K, V>(
542 &self,
543 method: reqwest::Method,
544 path: P,
545 params: Params<'_, K, V>,
546 json_body: Option<&B>,
547 ) -> Result<Response, HttpClientError>
548 where
549 P: RequestPath + Send + Sync,
550 B: Serialize + ?Sized + Sync,
551 K: AsRef<str> + Sync,
552 V: AsRef<str> + Sync,
553 {
554 let req = self.create_request(method, path, params, json_body)?;
555 self.send(req).await
556 }
557}
558
559pub struct ClientBuilder {
562 urls: Vec<Url>,
563
564 timeout: Option<Duration>,
565 custom_user_agent: bool,
566 reqwest_client_builder: reqwest::ClientBuilder,
567 #[allow(dead_code)] use_secure_dns: bool,
569
570 #[cfg(feature = "tunneling")]
571 front: Option<fronted::Front>,
572
573 retry_limit: usize,
574 serialization: SerializationFormat,
575}
576
577impl ClientBuilder {
578 pub fn new<U>(url: U) -> Result<Self, HttpClientError>
582 where
583 U: IntoUrl,
584 {
585 let str_url = url.as_str();
586
587 if !str_url.starts_with("http") {
589 let alt = format!("http://{str_url}");
590 warn!(
591 "the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."
592 );
593 Self::new(alt)
595 } else {
596 let url = url.to_url()?;
597 Self::new_with_urls(vec![url])
598 }
599 }
600
601 #[cfg(feature = "network-defaults")]
603 #[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
606 pub fn from_network(
607 network: &nym_network_defaults::NymNetworkDetails,
608 ) -> Result<Self, HttpClientError> {
609 let urls = network.nym_api_urls.as_ref().cloned().unwrap_or_default();
610 Self::new_with_fronted_urls(urls.clone())
611 }
612
613 #[cfg(feature = "network-defaults")]
615 pub fn new_with_fronted_urls(
616 urls: Vec<nym_network_defaults::ApiUrl>,
617 ) -> Result<Self, HttpClientError> {
618 let urls = urls
619 .into_iter()
620 .map(|api_url| {
621 let mut url = Url::parse(&api_url.url)?;
623
624 #[cfg(feature = "tunneling")]
626 if let Some(ref front_hosts) = api_url.front_hosts {
627 let fronts: Vec<String> = front_hosts
628 .iter()
629 .map(|host| format!("https://{}", host))
630 .collect();
631 url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
632 HttpClientError::MalformedUrl {
633 raw: api_url.url.clone(),
634 source,
635 }
636 })?;
637 }
638
639 Ok(url)
640 })
641 .collect::<Result<Vec<_>, HttpClientError>>()?;
642
643 let mut builder = Self::new_with_urls(urls)?;
644
645 #[cfg(feature = "tunneling")]
647 {
648 builder = builder.with_fronting(FrontPolicy::OnRetry);
649 }
650
651 Ok(builder)
652 }
653
654 pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
656 if urls.is_empty() {
657 return Err(HttpClientError::NoUrlsProvided);
658 }
659
660 let urls = Self::check_urls(urls);
661
662 #[cfg(target_arch = "wasm32")]
663 let reqwest_client_builder = reqwest::ClientBuilder::new();
664
665 #[cfg(not(target_arch = "wasm32"))]
666 let reqwest_client_builder = default_builder();
667
668 Ok(ClientBuilder {
669 urls,
670 timeout: None,
671 custom_user_agent: false,
672 reqwest_client_builder,
673 use_secure_dns: true,
674 #[cfg(feature = "tunneling")]
675 front: None,
676
677 retry_limit: 0,
678 serialization: SerializationFormat::Json,
679 })
680 }
681
682 pub fn add_url(mut self, url: Url) -> Self {
684 self.urls.push(url);
685 self
686 }
687
688 fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
689 urls = urls.into_iter().unique().collect();
691
692 urls.iter()
694 .filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
695 .for_each(|url| {
696 warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
697 });
698
699 urls
700 }
701
702 pub fn with_timeout(mut self, timeout: Duration) -> Self {
708 self.timeout = Some(timeout);
709 self
710 }
711
712 pub fn with_retries(mut self, retry_limit: usize) -> Self {
720 self.retry_limit = retry_limit;
721 self
722 }
723
724 pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
726 self.reqwest_client_builder = reqwest_builder;
727 self
728 }
729
730 pub fn with_user_agent<V>(mut self, value: V) -> Self
732 where
733 V: TryInto<HeaderValue>,
734 V::Error: Into<http::Error>,
735 {
736 self.custom_user_agent = true;
737 self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
738 self
739 }
740
741 #[cfg(not(target_arch = "wasm32"))]
746 pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
747 self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
748 self
749 }
750
751 pub fn with_serialization(mut self, format: SerializationFormat) -> Self {
753 self.serialization = format;
754 self
755 }
756
757 pub fn with_bincode(self) -> Self {
759 self.with_serialization(SerializationFormat::Bincode)
760 }
761
762 pub fn build(self) -> Result<Client, HttpClientError> {
764 #[cfg(target_arch = "wasm32")]
765 let reqwest_client = self.reqwest_client_builder.build()?;
766
767 #[cfg(not(target_arch = "wasm32"))]
770 let reqwest_client = {
771 let mut builder = self.reqwest_client_builder;
772
773 if self.use_secure_dns {
775 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
776 }
777
778 builder
779 .build()
780 .map_err(HttpClientError::reqwest_client_build_error)?
781 };
782
783 let client = Client {
784 base_urls: self.urls,
785 current_idx: Arc::new(AtomicUsize::new(0)),
786 reqwest_client,
787 using_secure_dns: self.use_secure_dns,
788
789 #[cfg(feature = "tunneling")]
790 front: self.front,
791
792 #[cfg(target_arch = "wasm32")]
793 request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
794 retry_limit: self.retry_limit,
795 serialization: self.serialization,
796 };
797
798 Ok(client)
799 }
800}
801
802#[derive(Debug, Clone)]
804pub struct Client {
805 base_urls: Vec<Url>,
806 current_idx: Arc<AtomicUsize>,
807 reqwest_client: reqwest::Client,
808 using_secure_dns: bool,
809
810 #[cfg(feature = "tunneling")]
811 front: Option<fronted::Front>,
812
813 #[cfg(target_arch = "wasm32")]
814 request_timeout: Duration,
815
816 retry_limit: usize,
817 serialization: SerializationFormat,
818}
819
820impl Client {
821 pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
827 Self::new_url(base_url, timeout).expect(
828 "we provided valid url and we were unwrapping previous construction errors anyway",
829 )
830 }
831
832 pub fn new_url<U>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError>
834 where
835 U: IntoUrl,
836 {
837 let builder = Self::builder(url)?;
838 match timeout {
839 Some(timeout) => builder.with_timeout(timeout).build(),
840 None => builder.build(),
841 }
842 }
843
844 pub fn builder<U>(url: U) -> Result<ClientBuilder, HttpClientError>
848 where
849 U: IntoUrl,
850 {
851 ClientBuilder::new(url)
852 }
853
854 pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
856 self.current_idx.store(0, Ordering::Relaxed);
857 self.base_urls = new_urls
858 }
859
860 pub fn clone_with_new_url(&self, new_url: Url) -> Self {
862 Client {
863 base_urls: vec![new_url],
864 current_idx: Arc::new(Default::default()),
865 reqwest_client: self.reqwest_client.clone(),
866 using_secure_dns: self.using_secure_dns,
867
868 #[cfg(feature = "tunneling")]
869 front: self.front.clone(),
870 retry_limit: self.retry_limit,
871
872 #[cfg(target_arch = "wasm32")]
873 request_timeout: self.request_timeout,
874 serialization: self.serialization,
875 }
876 }
877
878 pub fn current_url(&self) -> &Url {
880 &self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
881 }
882
883 pub fn base_urls(&self) -> &[Url] {
885 &self.base_urls
886 }
887
888 pub fn base_urls_mut(&mut self) -> &mut [Url] {
890 &mut self.base_urls
891 }
892
893 pub fn change_retry_limit(&mut self, limit: usize) {
895 self.retry_limit = limit;
896 }
897
898 #[cfg(feature = "tunneling")]
899 fn matches_current_host(&self, url: &Url) -> bool {
900 if let Some(ref front) = self.front
901 && front.is_enabled()
902 {
903 url.host_str() == self.current_url().front_str()
904 } else {
905 url.host_str() == self.current_url().host_str()
906 }
907 }
908
909 #[cfg(not(feature = "tunneling"))]
910 fn matches_current_host(&self, url: &Url) -> bool {
911 url.host_str() == self.current_url().host_str()
912 }
913
914 fn update_host(&self, maybe_url: Option<Url>) {
921 if let Some(err_url) = maybe_url
923 && !self.matches_current_host(&err_url)
924 {
925 return;
926 }
927
928 #[cfg(feature = "tunneling")]
929 if let Some(ref front) = self.front
930 && front.is_enabled()
931 {
932 let url = self.current_url();
934
935 if url.has_front() && !url.update() {
938 return;
940 }
941 }
942
943 if self.base_urls.len() > 1 {
944 let orig = self.current_idx.load(Ordering::Relaxed);
945
946 #[allow(unused_mut)]
947 let mut next = (orig + 1) % self.base_urls.len();
948
949 #[cfg(feature = "tunneling")]
951 if let Some(ref front) = self.front
952 && front.is_enabled()
953 {
954 while next != orig {
955 if self.base_urls[next].has_front() {
956 break;
958 }
959
960 next = (next + 1) % self.base_urls.len();
961 }
962 }
963
964 self.current_idx.store(next, Ordering::Relaxed);
965 debug!(
966 "http client rotating host {} -> {}",
967 self.base_urls[orig], self.base_urls[next]
968 );
969 }
970 }
971
972 fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
985 let url = self.current_url();
986 r.url_mut().set_host(url.host_str()).unwrap();
987
988 #[cfg(feature = "tunneling")]
989 if let Some(ref front) = self.front
990 && front.is_enabled()
991 {
992 if let Some(front_host) = url.front_str() {
993 if let Some(actual_host) = url.host_str() {
994 tracing::debug!(
995 "Domain fronting enabled: routing via CDN {} to actual host {}",
996 front_host,
997 actual_host
998 );
999
1000 r.url_mut().set_host(Some(front_host)).unwrap();
1002
1003 let actual_host_header: HeaderValue =
1004 actual_host.parse().unwrap_or(HeaderValue::from_static(""));
1005 _ = r
1008 .headers_mut()
1009 .insert(reqwest::header::HOST, actual_host_header);
1010
1011 return (url.as_str(), url.front_str());
1012 } else {
1013 tracing::debug!(
1014 "Domain fronting is enabled, but no host_url is defined for current URL"
1015 )
1016 }
1017 } else {
1018 tracing::debug!(
1019 "Domain fronting is enabled, but current URL has no front_hosts configured"
1020 )
1021 }
1022 }
1023 (url.as_str(), None)
1024 }
1025}
1026
1027#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1028#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1029impl ApiClientCore for Client {
1030 #[instrument(level = "debug", skip_all, fields(path=?path))]
1031 fn create_request<P, B, K, V>(
1032 &self,
1033 method: reqwest::Method,
1034 path: P,
1035 params: Params<'_, K, V>,
1036 body: Option<&B>,
1037 ) -> Result<RequestBuilder, HttpClientError>
1038 where
1039 P: RequestPath,
1040 B: Serialize + ?Sized,
1041 K: AsRef<str>,
1042 V: AsRef<str>,
1043 {
1044 let url = self.current_url();
1045 let url = sanitize_url(url, path, params);
1046
1047 let mut req = reqwest::Request::new(method, url.into());
1048
1049 self.apply_hosts_to_req(&mut req);
1050
1051 let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
1052
1053 rb = rb
1054 .header(ACCEPT, self.serialization.content_type())
1055 .header(CONTENT_TYPE, self.serialization.content_type());
1056
1057 if let Some(body) = body {
1058 match self.serialization {
1059 SerializationFormat::Json => {
1060 rb = rb.json(body);
1061 }
1062 SerializationFormat::Bincode => {
1063 let body = bincode::serialize(body)?;
1064 rb = rb.body(body);
1065 }
1066 SerializationFormat::Yaml => {
1067 let mut body_bytes = Vec::new();
1068 serde_yaml::to_writer(&mut body_bytes, &body)?;
1069 rb = rb.body(body_bytes);
1070 }
1071 SerializationFormat::Text => {
1072 let body = serde_plain::to_string(&body)?.as_bytes().to_vec();
1073 rb = rb.body(body);
1074 }
1075 }
1076 }
1077
1078 Ok(rb)
1079 }
1080
1081 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError> {
1082 let mut attempts = 0;
1083 loop {
1084 let r = request
1086 .try_clone()
1087 .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?;
1088
1089 let mut req = r
1092 .build()
1093 .map_err(HttpClientError::reqwest_client_build_error)?;
1094 self.apply_hosts_to_req(&mut req);
1095 let url: Url = req.url().clone().into();
1096
1097 #[cfg(target_arch = "wasm32")]
1098 let response: Result<Response, HttpClientError> = {
1099 Ok(wasmtimer::tokio::timeout(
1100 self.request_timeout,
1101 self.reqwest_client.execute(req),
1102 )
1103 .await
1104 .map_err(|_timeout| HttpClientError::RequestTimeout)??)
1105 };
1106
1107 #[cfg(not(target_arch = "wasm32"))]
1108 let response = self.reqwest_client.execute(req).await;
1109
1110 match response {
1111 Ok(resp) => return Ok(resp),
1112 Err(err) => {
1113 #[cfg(target_arch = "wasm32")]
1118 let is_network_err = err.is_timeout();
1119 #[cfg(not(target_arch = "wasm32"))]
1120 let is_network_err = err.is_timeout() || err.is_connect();
1121
1122 if is_network_err {
1123 self.update_host(Some(url.clone()));
1125
1126 #[cfg(feature = "tunneling")]
1127 if let Some(ref front) = self.front {
1128 let was_enabled = front.is_enabled();
1131 front.retry_enable();
1132 if !was_enabled && front.is_enabled() {
1133 tracing::info!(
1134 "Domain fronting activated after connection failure: {err}",
1135 );
1136 }
1137 }
1138 }
1139
1140 if attempts < self.retry_limit {
1141 attempts += 1;
1142 warn!(
1143 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1144 self.retry_limit
1145 );
1146 continue;
1147 }
1148
1149 cfg_if::cfg_if! {
1151 if #[cfg(target_arch = "wasm32")] {
1152 return Err(err);
1153 } else {
1154 return Err(HttpClientError::request_send_error(url.into(), err));
1155 }
1156 }
1157 }
1158 }
1159 }
1160 }
1161}
1162
1163#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1167#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1168pub trait ApiClient: ApiClientCore {
1169 fn create_get_request<P, K, V>(
1171 &self,
1172 path: P,
1173 params: Params<'_, K, V>,
1174 ) -> Result<RequestBuilder, HttpClientError>
1175 where
1176 P: RequestPath,
1177 K: AsRef<str>,
1178 V: AsRef<str>,
1179 {
1180 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1181 }
1182
1183 fn create_post_request<P, B, K, V>(
1185 &self,
1186 path: P,
1187 params: Params<'_, K, V>,
1188 json_body: &B,
1189 ) -> Result<RequestBuilder, HttpClientError>
1190 where
1191 P: RequestPath,
1192 B: Serialize + ?Sized,
1193 K: AsRef<str>,
1194 V: AsRef<str>,
1195 {
1196 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1197 }
1198
1199 fn create_delete_request<P, K, V>(
1201 &self,
1202 path: P,
1203 params: Params<'_, K, V>,
1204 ) -> Result<RequestBuilder, HttpClientError>
1205 where
1206 P: RequestPath,
1207 K: AsRef<str>,
1208 V: AsRef<str>,
1209 {
1210 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1211 }
1212
1213 fn create_patch_request<P, B, K, V>(
1215 &self,
1216 path: P,
1217 params: Params<'_, K, V>,
1218 json_body: &B,
1219 ) -> Result<RequestBuilder, HttpClientError>
1220 where
1221 P: RequestPath,
1222 B: Serialize + ?Sized,
1223 K: AsRef<str>,
1224 V: AsRef<str>,
1225 {
1226 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1227 }
1228
1229 #[instrument(level = "debug", skip_all, fields(path=?path))]
1231 async fn send_get_request<P, K, V>(
1232 &self,
1233 path: P,
1234 params: Params<'_, K, V>,
1235 ) -> Result<Response, HttpClientError>
1236 where
1237 P: RequestPath + Send + Sync,
1238 K: AsRef<str> + Sync,
1239 V: AsRef<str> + Sync,
1240 {
1241 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1242 .await
1243 }
1244
1245 async fn send_post_request<P, B, K, V>(
1247 &self,
1248 path: P,
1249 params: Params<'_, K, V>,
1250 json_body: &B,
1251 ) -> Result<Response, HttpClientError>
1252 where
1253 P: RequestPath + Send + Sync,
1254 B: Serialize + ?Sized + Sync,
1255 K: AsRef<str> + Sync,
1256 V: AsRef<str> + Sync,
1257 {
1258 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1259 .await
1260 }
1261
1262 async fn send_delete_request<P, K, V>(
1264 &self,
1265 path: P,
1266 params: Params<'_, K, V>,
1267 ) -> Result<Response, HttpClientError>
1268 where
1269 P: RequestPath + Send + Sync,
1270 K: AsRef<str> + Sync,
1271 V: AsRef<str> + Sync,
1272 {
1273 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1274 .await
1275 }
1276
1277 async fn send_patch_request<P, B, K, V>(
1279 &self,
1280 path: P,
1281 params: Params<'_, K, V>,
1282 json_body: &B,
1283 ) -> Result<Response, HttpClientError>
1284 where
1285 P: RequestPath + Send + Sync,
1286 B: Serialize + ?Sized + Sync,
1287 K: AsRef<str> + Sync,
1288 V: AsRef<str> + Sync,
1289 {
1290 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1291 .await
1292 }
1293
1294 #[instrument(level = "debug", skip_all, fields(path=?path))]
1298 async fn get_json<P, T, K, V>(
1300 &self,
1301 path: P,
1302 params: Params<'_, K, V>,
1303 ) -> Result<T, HttpClientError>
1304 where
1305 P: RequestPath + Send + Sync,
1306 for<'a> T: Deserialize<'a>,
1307 K: AsRef<str> + Sync,
1308 V: AsRef<str> + Sync,
1309 {
1310 self.get_response(path, params).await
1311 }
1312
1313 async fn get_response<P, T, K, V>(
1317 &self,
1318 path: P,
1319 params: Params<'_, K, V>,
1320 ) -> Result<T, HttpClientError>
1321 where
1322 P: RequestPath + Send + Sync,
1323 for<'a> T: Deserialize<'a>,
1324 K: AsRef<str> + Sync,
1325 V: AsRef<str> + Sync,
1326 {
1327 let res = self
1328 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1329 .await?;
1330 parse_response(res, false).await
1331 }
1332
1333 async fn post_json<P, B, T, K, V>(
1337 &self,
1338 path: P,
1339 params: Params<'_, K, V>,
1340 json_body: &B,
1341 ) -> Result<T, HttpClientError>
1342 where
1343 P: RequestPath + Send + Sync,
1344 B: Serialize + ?Sized + Sync,
1345 for<'a> T: Deserialize<'a>,
1346 K: AsRef<str> + Sync,
1347 V: AsRef<str> + Sync,
1348 {
1349 let res = self
1350 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1351 .await?;
1352 parse_response(res, false).await
1353 }
1354
1355 async fn delete_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 let res = self
1370 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1371 .await?;
1372 parse_response(res, false).await
1373 }
1374
1375 async fn patch_json<P, B, T, K, V>(
1379 &self,
1380 path: P,
1381 params: Params<'_, K, V>,
1382 json_body: &B,
1383 ) -> Result<T, HttpClientError>
1384 where
1385 P: RequestPath + Send + Sync,
1386 B: Serialize + ?Sized + Sync,
1387 for<'a> T: Deserialize<'a>,
1388 K: AsRef<str> + Sync,
1389 V: AsRef<str> + Sync,
1390 {
1391 let res = self
1392 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1393 .await?;
1394 parse_response(res, false).await
1395 }
1396
1397 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1400 where
1401 for<'a> T: Deserialize<'a>,
1402 S: AsRef<str> + Sync + Send,
1403 {
1404 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1405 let res = self.send(req).await?;
1406 parse_response(res, false).await
1407 }
1408
1409 async fn post_json_data_to<B, T, S>(
1412 &self,
1413 endpoint: S,
1414 json_body: &B,
1415 ) -> Result<T, HttpClientError>
1416 where
1417 B: Serialize + ?Sized + Sync,
1418 for<'a> T: Deserialize<'a>,
1419 S: AsRef<str> + Sync + Send,
1420 {
1421 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1422 let res = self.send(req).await?;
1423 parse_response(res, false).await
1424 }
1425
1426 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1429 where
1430 for<'a> T: Deserialize<'a>,
1431 S: AsRef<str> + Sync + Send,
1432 {
1433 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1434 let res = self.send(req).await?;
1435 parse_response(res, false).await
1436 }
1437
1438 async fn patch_json_data_at<B, T, S>(
1441 &self,
1442 endpoint: S,
1443 json_body: &B,
1444 ) -> Result<T, HttpClientError>
1445 where
1446 B: Serialize + ?Sized + Sync,
1447 for<'a> T: Deserialize<'a>,
1448 S: AsRef<str> + Sync + Send,
1449 {
1450 let req =
1451 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1452 let res = self.send(req).await?;
1453 parse_response(res, false).await
1454 }
1455}
1456
1457#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1458#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1459impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1460
1461fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1463 base: &Url,
1464 request_path: impl RequestPath,
1465 params: Params<'_, K, V>,
1466) -> Url {
1467 let mut url = base.clone();
1468 let mut path_segments = url
1469 .path_segments_mut()
1470 .expect("provided validator url does not have a base!");
1471
1472 path_segments.pop_if_empty();
1473
1474 for segment in request_path.to_sanitized_segments() {
1475 path_segments.push(segment);
1476 }
1477
1478 drop(path_segments);
1481
1482 if !params.is_empty() {
1483 url.query_pairs_mut().extend_pairs(params);
1484 }
1485
1486 url
1487}
1488
1489fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1490 use encoding_rs::{Encoding, UTF_8};
1491
1492 let content_type = try_get_mime_type(headers);
1493
1494 let encoding_name = content_type
1495 .as_ref()
1496 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1497 .unwrap_or("utf-8");
1498
1499 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1500
1501 let (text, _, _) = encoding.decode(bytes);
1502 text.into_owned()
1503}
1504
1505#[instrument(level = "debug", skip_all)]
1507pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1508where
1509 T: DeserializeOwned,
1510{
1511 let status = res.status();
1512 let headers = res.headers().clone();
1513 let url = res.url().clone();
1514
1515 tracing::trace!("status: {status} (success: {})", status.is_success());
1516 tracing::trace!("headers: {headers:?}");
1517
1518 if !allow_empty && let Some(0) = res.content_length() {
1519 return Err(HttpClientError::EmptyResponse {
1520 url,
1521 status,
1522 headers: Box::new(headers),
1523 });
1524 }
1525
1526 if res.status().is_success() {
1527 let full = res
1530 .bytes()
1531 .await
1532 .map_err(|source| HttpClientError::ResponseReadFailure {
1533 url,
1534 headers: Box::new(headers.clone()),
1535 status,
1536 source: ReqwestErrorWrapper(source),
1537 })?;
1538 decode_raw_response(&headers, full)
1539 } else if res.status() == StatusCode::NOT_FOUND {
1540 Err(HttpClientError::NotFound { url })
1541 } else {
1542 let Ok(plaintext) = res.text().await else {
1543 return Err(HttpClientError::RequestFailure {
1544 url,
1545 status,
1546 headers: Box::new(headers),
1547 });
1548 };
1549
1550 Err(HttpClientError::EndpointFailure {
1551 url,
1552 status,
1553 headers: Box::new(headers),
1554 error: plaintext,
1555 })
1556 }
1557}
1558
1559fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1560where
1561 T: DeserializeOwned,
1562{
1563 match serde_json::from_slice(&content) {
1564 Ok(data) => Ok(data),
1565 Err(err) => {
1566 let content = decode_as_text(&content, headers);
1567 Err(HttpClientError::ResponseDecodeFailure {
1568 message: err.to_string(),
1569 content,
1570 })
1571 }
1572 }
1573}
1574
1575fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1576where
1577 T: DeserializeOwned,
1578{
1579 use bincode::Options;
1580
1581 let opts = nym_http_api_common::make_bincode_serializer();
1582 match opts.deserialize(&content) {
1583 Ok(data) => Ok(data),
1584 Err(err) => {
1585 let content = decode_as_text(&content, headers);
1586 Err(HttpClientError::ResponseDecodeFailure {
1587 message: err.to_string(),
1588 content,
1589 })
1590 }
1591 }
1592}
1593
1594fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1595where
1596 T: DeserializeOwned,
1597{
1598 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
1600
1601 debug!("attempting to parse response as {mime}");
1602
1603 match (mime.type_(), mime.subtype().as_str()) {
1605 (mime::APPLICATION, "json") => decode_as_json(headers, content),
1606 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
1607 (_, _) => {
1608 debug!("unrecognised mime type {mime}. falling back to json decoding...");
1609 decode_as_json(headers, content)
1610 }
1611 }
1612}
1613
1614fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
1615 headers
1616 .get(CONTENT_TYPE)
1617 .and_then(|value| value.to_str().ok())
1618 .and_then(|value| value.parse::<Mime>().ok())
1619}
1620
1621#[cfg(test)]
1622mod tests;