1#![allow(deprecated)]
5#![warn(missing_docs)]
141
142use http::header::{RETRY_AFTER, USER_AGENT};
143pub use inventory;
144pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
145use std::error::Error;
146use std::sync::Mutex;
147#[cfg(not(target_arch = "wasm32"))]
148use std::time::Instant;
149#[cfg(target_arch = "wasm32")]
150use wasmtimer::std::Instant;
151
152pub mod registry;
153
154pub use crate::path::RequestPath;
158use async_trait::async_trait;
159use bytes::Bytes;
160use cfg_if::cfg_if;
161use http::{
162 HeaderMap,
163 header::{ACCEPT, CONTENT_TYPE},
164};
165use itertools::Itertools;
166use mime::Mime;
167use reqwest::{RequestBuilder, Response, header::HeaderValue};
168use serde::{Deserialize, Serialize, de::DeserializeOwned};
169#[cfg(not(target_arch = "wasm32"))]
170use std::io::ErrorKind;
171use std::{
172 fmt::Display,
173 sync::atomic::{AtomicUsize, Ordering},
174 time::Duration,
175};
176use thiserror::Error;
177use tracing::{debug, instrument, warn};
178
179use std::sync::{Arc, LazyLock};
180
181#[cfg(feature = "tunneling")]
182mod fronted;
183#[cfg(feature = "tunneling")]
184pub use fronted::FrontPolicy;
185mod url;
186pub use url::{IntoUrl, Url};
187mod user_agent;
188pub use user_agent::UserAgent;
189
190#[cfg(not(target_arch = "wasm32"))]
191pub mod dns;
192mod path;
193
194#[cfg(not(target_arch = "wasm32"))]
195pub use dns::{HickoryDnsResolver, ResolveError};
196
197#[cfg(not(target_arch = "wasm32"))]
199use crate::registry::default_builder;
200#[doc(hidden)]
201pub use nym_bin_common::bin_info;
202#[cfg(not(target_arch = "wasm32"))]
203use nym_http_api_client_macro::client_defaults;
204
205pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
210
211const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
212
213#[cfg(not(target_arch = "wasm32"))]
214client_defaults!(
215 priority = -100;
216 gzip = true,
217 deflate = true,
218 brotli = true,
219 zstd = true,
220 timeout = DEFAULT_TIMEOUT,
221 user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
222);
223
224static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
225 tracing::info!("Initializing shared HTTP client");
226 cfg_if! {
227 if #[cfg(target_arch = "wasm32")] {
228 reqwest::ClientBuilder::new().build()
229 .expect("failed to initialize shared http client")
230 } else {
231 let mut builder = default_builder();
232
233 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::new()));
234
235 builder
236 .build()
237 .expect("failed to initialize shared http client")
238 }
239 }
240});
241
242pub(crate) static SHARED_NETWORK_RECONFIGURATION: LazyLock<Arc<Mutex<Option<Instant>>>> =
243 LazyLock::new(|| Arc::new(Mutex::new(None)));
244
245pub fn network_reconfigured() {
247 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap() = Some(Instant::now());
248}
249
250pub type PathSegments<'a> = &'a [&'a str];
252pub type Params<'a, K, V> = &'a [(K, V)];
254
255pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum SerializationFormat {
261 Json,
263 Bincode,
265 Yaml,
267 Text,
269}
270
271impl Display for SerializationFormat {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 match self {
274 SerializationFormat::Json => write!(f, "json"),
275 SerializationFormat::Bincode => write!(f, "bincode"),
276 SerializationFormat::Yaml => write!(f, "yaml"),
277 SerializationFormat::Text => write!(f, "text"),
278 }
279 }
280}
281
282impl SerializationFormat {
283 #[allow(missing_docs)]
284 pub fn content_type(&self) -> String {
285 match self {
286 SerializationFormat::Json => "application/json".to_string(),
287 SerializationFormat::Bincode => "application/bincode".to_string(),
288 SerializationFormat::Yaml => "application/yaml".to_string(),
289 SerializationFormat::Text => "text/plain".to_string(),
290 }
291 }
292}
293
294#[allow(missing_docs)]
295#[derive(Debug)]
296pub struct ReqwestErrorWrapper(reqwest::Error);
297
298impl Display for ReqwestErrorWrapper {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 cfg_if::cfg_if! {
301 if #[cfg(not(target_arch = "wasm32"))] {
302 if self.0.is_connect() {
303 write!(f, "failed to connect: ")?;
304 }
305 }
306 }
307
308 if self.0.is_timeout() {
309 write!(f, "timed out: ")?;
310 }
311 if self.0.is_redirect()
312 && let Some(final_stop) = self.0.url()
313 {
314 write!(f, "redirect loop at {final_stop}: ")?;
315 }
316
317 self.0.fmt(f)?;
318 if let Some(status_code) = self.0.status() {
319 write!(f, " status: {status_code}")?;
320 } else {
321 write!(f, " unknown status code")?;
322 }
323
324 if let Some(source) = self.0.source() {
325 write!(f, " source: {source:?}")?;
326 } else {
327 write!(f, " unknown lower-level error source")?;
328 }
329
330 Ok(())
331 }
332}
333
334impl std::error::Error for ReqwestErrorWrapper {}
335
336#[derive(Debug, Error)]
338#[allow(missing_docs)]
339pub enum HttpClientError {
340 #[error("did not provide any valid client URLs")]
341 NoUrlsProvided,
342
343 #[error("failed to construct inner reqwest client: {source}")]
344 ReqwestBuildError {
345 #[source]
346 source: reqwest::Error,
347 },
348
349 #[deprecated(
350 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
351 )]
352 #[error("request failed with error message: {0}")]
353 GenericRequestFailure(String),
354
355 #[deprecated(
356 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
357 )]
358 #[error("there was an issue with the REST request: {source}")]
359 ReqwestClientError {
360 #[from]
361 source: reqwest::Error,
362 },
363
364 #[error("failed to parse {raw} as a valid URL: {source}")]
365 MalformedUrl {
366 raw: String,
367 #[source]
368 source: reqwest::Error,
369 },
370
371 #[error("failed to parse header value: {source}")]
372 InvalidHeaderValue {
373 #[source]
374 source: http::Error,
375 },
376
377 #[error("failed to send request for {url}: {source}")]
378 RequestSendFailure {
379 url: Box<reqwest::Url>,
380 #[source]
381 source: ReqwestErrorWrapper,
382 },
383
384 #[error("failed to read response body from {url}: {source}")]
385 ResponseReadFailure {
386 url: Box<reqwest::Url>,
387 headers: Box<HeaderMap>,
388 status: StatusCode,
389 #[source]
390 source: ReqwestErrorWrapper,
391 },
392
393 #[error("failed to deserialize received response: {source}")]
394 ResponseDeserialisationFailure { source: serde_json::Error },
395
396 #[error("provided url is malformed: {source}")]
397 UrlParseFailure {
398 #[from]
399 source: url::ParseError,
400 },
401
402 #[error("the requested resource could not be found at {url}")]
403 NotFound { url: Box<reqwest::Url> },
404
405 #[error("attempted to use domain fronting and clone a request containing stream data")]
406 AttemptedToCloneStreamRequest,
407
408 #[error(
412 "the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
413 )]
414 RequestFailure {
415 url: Box<reqwest::Url>,
416 status: StatusCode,
417 headers: Box<HeaderMap>,
418 },
419
420 #[error(
421 "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
422 )]
423 EmptyResponse {
424 url: Box<reqwest::Url>,
425 status: StatusCode,
426 headers: Box<HeaderMap>,
427 },
428
429 #[error(
430 "failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
431 )]
432 EndpointFailure {
433 url: Box<reqwest::Url>,
434 status: StatusCode,
435 headers: Box<HeaderMap>,
436 error: String,
437 },
438
439 #[error("failed to decode response body: {message} from {content}")]
440 ResponseDecodeFailure { message: String, content: String },
441
442 #[error("failed to resolve request to {url} due to data inconsistency: {details}")]
443 InternalResponseInconsistency { url: ::url::Url, details: String },
444
445 #[cfg(not(target_arch = "wasm32"))]
446 #[error("encountered dns failure: {inner}")]
447 DnsLookupFailure {
448 #[from]
449 inner: ResolveError,
450 },
451
452 #[error("Failed to encode bincode: {0}")]
453 Bincode(#[from] bincode::Error),
454
455 #[error("Failed to json: {0}")]
456 Json(#[from] serde_json::Error),
457
458 #[error("Failed to yaml: {0}")]
459 Yaml(#[from] serde_yaml::Error),
460
461 #[error("Failed to plain: {0}")]
462 Plain(#[from] serde_plain::Error),
463
464 #[cfg(target_arch = "wasm32")]
465 #[error("the request has timed out")]
466 RequestTimeout,
467}
468
469#[allow(missing_docs)]
470#[allow(deprecated)]
471impl HttpClientError {
472 pub fn is_timeout(&self) -> bool {
474 match self {
475 HttpClientError::ReqwestClientError { source } => source.is_timeout(),
476 HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
477 HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
478 #[cfg(not(target_arch = "wasm32"))]
479 HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
480 #[cfg(target_arch = "wasm32")]
481 HttpClientError::RequestTimeout => true,
482 _ => false,
483 }
484 }
485
486 pub fn status_code(&self) -> Option<StatusCode> {
488 match self {
489 HttpClientError::ResponseReadFailure { status, .. } => Some(*status),
490 HttpClientError::RequestFailure { status, .. } => Some(*status),
491 HttpClientError::EmptyResponse { status, .. } => Some(*status),
492 HttpClientError::EndpointFailure { status, .. } => Some(*status),
493 _ => None,
494 }
495 }
496
497 pub fn reqwest_client_build_error(source: reqwest::Error) -> Self {
498 HttpClientError::ReqwestBuildError { source }
499 }
500
501 pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
502 HttpClientError::RequestSendFailure {
503 url: Box::new(url),
504 source: ReqwestErrorWrapper(source),
505 }
506 }
507
508 pub fn is_data_inconsistency(&self) -> bool {
509 matches!(self, HttpClientError::InternalResponseInconsistency { .. })
510 }
511}
512
513#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
519#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
520pub trait ApiClientCore {
521 fn create_request<P, B, K, V>(
523 &self,
524 method: reqwest::Method,
525 path: P,
526 params: Params<'_, K, V>,
527 body: Option<&B>,
528 ) -> Result<RequestBuilder, HttpClientError>
529 where
530 P: RequestPath,
531 B: Serialize + ?Sized,
532 K: AsRef<str>,
533 V: AsRef<str>;
534
535 fn create_request_endpoint<B, S>(
551 &self,
552 method: reqwest::Method,
553 endpoint: S,
554 body: Option<&B>,
555 ) -> Result<RequestBuilder, HttpClientError>
556 where
557 B: Serialize + ?Sized,
558 S: AsRef<str>,
559 {
560 let mut standin_url: Url = "http://example.com".parse().unwrap();
565
566 match endpoint.as_ref().split_once("?") {
567 Some((path, query)) => {
568 standin_url.set_path(path);
569 standin_url.set_query(Some(query));
570 }
571 None => standin_url.set_path(endpoint.as_ref()),
573 }
574
575 let path: Vec<&str> = match standin_url.path_segments() {
576 Some(segments) => segments.collect(),
577 None => Vec::new(),
578 };
579 let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
580
581 self.create_request(method, path.as_slice(), ¶ms, body)
582 }
583
584 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError>;
590
591 async fn send_request<P, B, K, V>(
593 &self,
594 method: reqwest::Method,
595 path: P,
596 params: Params<'_, K, V>,
597 json_body: Option<&B>,
598 ) -> Result<Response, HttpClientError>
599 where
600 P: RequestPath + Send + Sync,
601 B: Serialize + ?Sized + Sync,
602 K: AsRef<str> + Sync,
603 V: AsRef<str> + Sync,
604 {
605 let req = self.create_request(method, path, params, json_body)?;
606 self.send(req).await
607 }
608
609 fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
616
617 #[cfg(feature = "tunneling")]
620 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
621}
622
623pub struct ClientBuilder {
626 urls: Vec<Url>,
627
628 timeout: Option<Duration>,
629 custom_user_agent: Option<HeaderValue>,
630 reqwest_client_builder: Option<reqwest::ClientBuilder>,
631 #[allow(dead_code)] use_secure_dns: bool,
633
634 #[cfg(feature = "tunneling")]
635 front: fronted::Front,
636
637 retry_limit: usize,
638 serialization: SerializationFormat,
639
640 error: Option<HttpClientError>,
641}
642
643impl ClientBuilder {
644 pub fn new<U>(url: U) -> Result<Self, HttpClientError>
648 where
649 U: IntoUrl,
650 {
651 let str_url = url.as_str();
652
653 if !str_url.starts_with("http") {
655 let alt = format!("http://{str_url}");
656 warn!(
657 "the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."
658 );
659 Self::new(alt)
661 } else {
662 let url = url.to_url()?;
663 Self::new_with_urls(vec![url])
664 }
665 }
666
667 #[cfg(feature = "network-defaults")]
669 #[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
672 pub fn from_network(
673 network: &nym_network_defaults::NymNetworkDetails,
674 ) -> Result<Self, HttpClientError> {
675 let urls = network.nym_api_urls();
676 Self::new_with_fronted_urls(urls.clone())
677 }
678
679 #[cfg(feature = "network-defaults")]
681 pub fn new_with_fronted_urls(
682 urls: Vec<nym_network_defaults::ApiUrl>,
683 ) -> Result<Self, HttpClientError> {
684 let urls = urls
685 .into_iter()
686 .map(|api_url| {
687 let mut url = Url::parse(&api_url.url)?;
689
690 #[cfg(feature = "tunneling")]
692 if let Some(ref front_hosts) = api_url.front_hosts {
693 let fronts: Vec<String> = front_hosts
694 .iter()
695 .map(|host| format!("https://{}", host))
696 .collect();
697 url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
698 HttpClientError::MalformedUrl {
699 raw: api_url.url.clone(),
700 source,
701 }
702 })?;
703 }
704
705 Ok(url)
706 })
707 .collect::<Result<Vec<_>, HttpClientError>>()?;
708
709 let mut builder = Self::new_with_urls(urls)?;
710
711 #[cfg(feature = "tunneling")]
713 {
714 builder = builder.with_fronting(None);
715 }
716
717 Ok(builder)
718 }
719
720 pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
722 if urls.is_empty() {
723 return Err(HttpClientError::NoUrlsProvided);
724 }
725
726 let urls = Self::check_urls(urls);
727
728 Ok(ClientBuilder {
729 urls,
730 timeout: None,
731 custom_user_agent: None,
732 reqwest_client_builder: None,
733 use_secure_dns: true,
734 #[cfg(feature = "tunneling")]
735 front: fronted::Front::off(),
736
737 retry_limit: 0,
738 serialization: SerializationFormat::Json,
739 error: None,
740 })
741 }
742
743 #[cfg(not(target_arch = "wasm32"))]
746 pub fn non_shared(mut self) -> Self {
747 if self.reqwest_client_builder.is_none() {
748 self.reqwest_client_builder = Some(default_builder());
749 }
750 self
751 }
752
753 pub fn add_url(mut self, url: Url) -> Self {
755 self.urls.push(url);
756 self
757 }
758
759 fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
760 urls = urls.into_iter().unique().collect();
762
763 urls.iter()
765 .filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
766 .for_each(|url| {
767 warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
768 });
769
770 urls
771 }
772
773 #[must_use]
779 pub fn with_timeout(mut self, timeout: Duration) -> Self {
780 self.timeout = Some(timeout);
781 self
782 }
783
784 #[must_use]
792 pub fn with_retries(mut self, retry_limit: usize) -> Self {
793 self.retry_limit = retry_limit;
794 self
795 }
796
797 #[must_use]
799 pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
800 self.reqwest_client_builder = Some(reqwest_builder);
801 self
802 }
803
804 #[must_use]
806 pub fn with_user_agent<V>(mut self, value: V) -> Self
807 where
808 V: TryInto<HeaderValue>,
809 V::Error: Into<http::Error>,
810 {
811 match value.try_into() {
812 Ok(v) => self.custom_user_agent = Some(v),
813 Err(err) => {
814 self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
815 }
816 }
817 self
818 }
819
820 #[must_use]
822 pub fn with_serialization(mut self, format: SerializationFormat) -> Self {
823 self.serialization = format;
824 self
825 }
826
827 #[must_use]
829 pub fn with_bincode(self) -> Self {
830 self.with_serialization(SerializationFormat::Bincode)
831 }
832
833 pub fn build(self) -> Result<Client, HttpClientError> {
835 if let Some(err) = self.error {
836 return Err(err);
837 }
838
839 #[cfg(target_arch = "wasm32")]
840 let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
841
842 #[cfg(not(target_arch = "wasm32"))]
843 let reqwest_client = self
844 .reqwest_client_builder
845 .map(|mut builder| {
846 if self.use_secure_dns {
848 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::new()));
849 }
850
851 builder
852 .build()
853 .map_err(HttpClientError::reqwest_client_build_error)
854 })
855 .transpose()?;
856
857 let client = Client {
858 base_urls: self.urls,
859 current_idx: Arc::new(AtomicUsize::new(0)),
860 reqwest_client,
861 custom_user_agent: self.custom_user_agent,
862
863 #[cfg(feature = "tunneling")]
864 front: self.front,
865
866 #[cfg(target_arch = "wasm32")]
867 request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
868 retry_limit: self.retry_limit,
869 serialization: self.serialization,
870 };
871
872 Ok(client)
873 }
874}
875
876#[derive(Debug, Clone)]
878pub struct Client {
879 base_urls: Vec<Url>,
880 current_idx: Arc<AtomicUsize>,
881 reqwest_client: Option<reqwest::Client>,
882 custom_user_agent: Option<HeaderValue>,
883
884 #[cfg(feature = "tunneling")]
885 front: fronted::Front,
886
887 #[cfg(target_arch = "wasm32")]
888 request_timeout: Duration,
889
890 retry_limit: usize,
891 serialization: SerializationFormat,
892}
893
894impl Client {
895 pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
901 Self::new_url(base_url, timeout).expect(
902 "we provided valid url and we were unwrapping previous construction errors anyway",
903 )
904 }
905
906 pub fn new_url<U>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError>
908 where
909 U: IntoUrl,
910 {
911 let builder = Self::builder(url)?;
912 match timeout {
913 Some(timeout) => builder.with_timeout(timeout).build(),
914 None => builder.build(),
915 }
916 }
917
918 pub fn builder<U>(url: U) -> Result<ClientBuilder, HttpClientError>
922 where
923 U: IntoUrl,
924 {
925 ClientBuilder::new(url)
926 }
927
928 pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
930 self.current_idx.store(0, Ordering::Relaxed);
931 self.base_urls = new_urls
932 }
933
934 pub fn clone_with_new_url(&self, new_url: Url) -> Self {
936 Client {
937 base_urls: vec![new_url],
938 current_idx: Arc::new(Default::default()),
939 reqwest_client: None,
940 custom_user_agent: None,
941
942 #[cfg(feature = "tunneling")]
943 front: self.front.clone(),
944 retry_limit: self.retry_limit,
945
946 #[cfg(target_arch = "wasm32")]
947 request_timeout: self.request_timeout,
948 serialization: self.serialization,
949 }
950 }
951
952 pub fn current_url(&self) -> &Url {
954 &self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
955 }
956
957 pub fn base_urls(&self) -> &[Url] {
959 &self.base_urls
960 }
961
962 pub fn base_urls_mut(&mut self) -> &mut [Url] {
964 &mut self.base_urls
965 }
966
967 pub fn change_retry_limit(&mut self, limit: usize) {
969 self.retry_limit = limit;
970 }
971
972 #[cfg(feature = "tunneling")]
973 fn matches_current_host(&self, url: &Url) -> bool {
974 if self.front.is_enabled() && self.current_url().has_front() {
977 url.host_str() == self.current_url().front_str()
978 } else {
979 url.host_str() == self.current_url().host_str()
980 }
981 }
982
983 #[cfg(not(feature = "tunneling"))]
984 fn matches_current_host(&self, url: &Url) -> bool {
985 url.host_str() == self.current_url().host_str()
986 }
987
988 fn update_host(&self, maybe_url: Option<Url>) {
995 if let Some(err_url) = maybe_url
997 && !self.matches_current_host(&err_url)
998 {
999 return;
1000 }
1001
1002 #[cfg(feature = "tunneling")]
1003 if self.front.is_enabled() {
1004 let url = self.current_url();
1006
1007 if url.has_front() && !url.update() {
1010 return;
1012 }
1013 }
1014
1015 if self.base_urls.len() > 1 {
1016 let orig = self.current_idx.load(Ordering::Relaxed);
1017
1018 #[allow(unused_mut)]
1019 let mut next = (orig + 1) % self.base_urls.len();
1020
1021 #[cfg(feature = "tunneling")]
1023 if self.front.is_enabled() {
1024 while next != orig {
1025 if self.base_urls[next].has_front() {
1026 break;
1028 }
1029
1030 next = (next + 1) % self.base_urls.len();
1031 }
1032 }
1033
1034 self.current_idx.store(next, Ordering::Relaxed);
1035 debug!(
1036 "http client rotating host {} -> {}",
1037 self.base_urls[orig], self.base_urls[next]
1038 );
1039 }
1040 }
1041
1042 pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
1055 let url = self.current_url();
1056 r.url_mut().set_host(url.host_str()).unwrap();
1057
1058 #[cfg(feature = "tunneling")]
1059 if self.front.is_enabled() {
1060 if let Some(front_host) = url.front_str() {
1061 if let Some(actual_host) = url.host_str() {
1062 tracing::debug!(
1063 "Domain fronting enabled: routing via CDN {} to actual host {}",
1064 front_host,
1065 actual_host
1066 );
1067
1068 r.url_mut().set_host(Some(front_host)).unwrap();
1070
1071 let actual_host_header: HeaderValue =
1072 actual_host.parse().unwrap_or(HeaderValue::from_static(""));
1073 _ = r
1076 .headers_mut()
1077 .insert(reqwest::header::HOST, actual_host_header);
1078
1079 let front_host_header: HeaderValue =
1081 front_host.parse().unwrap_or(HeaderValue::from_static(""));
1082 _ = r
1083 .headers_mut()
1084 .insert(NYM_OUTER_SNI_HEADER, front_host_header);
1085
1086 return (url.as_str(), url.front_str());
1087 } else {
1088 tracing::debug!(
1089 "Domain fronting is enabled, but no host_url is defined for current URL"
1090 )
1091 }
1092 } else {
1093 tracing::debug!(
1094 "Domain fronting is enabled, but current URL has no front_hosts configured"
1095 )
1096 }
1097 }
1098 (url.as_str(), None)
1099 }
1100}
1101
1102#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1103#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1104impl ApiClientCore for Client {
1105 #[instrument(level = "debug", skip_all, fields(path=?path))]
1106 fn create_request<P, B, K, V>(
1107 &self,
1108 method: reqwest::Method,
1109 path: P,
1110 params: Params<'_, K, V>,
1111 body: Option<&B>,
1112 ) -> Result<RequestBuilder, HttpClientError>
1113 where
1114 P: RequestPath,
1115 B: Serialize + ?Sized,
1116 K: AsRef<str>,
1117 V: AsRef<str>,
1118 {
1119 let url = self.current_url();
1120 let url = sanitize_url(url, path, params);
1121
1122 let mut req = reqwest::Request::new(method, url.into());
1123
1124 self.apply_hosts_to_req(&mut req);
1125
1126 let client = if let Some(client) = &self.reqwest_client {
1127 client.clone()
1128 } else {
1129 SHARED_CLIENT.clone()
1130 };
1131 let mut rb = RequestBuilder::from_parts(client, req);
1132
1133 rb = rb
1134 .header(ACCEPT, self.serialization.content_type())
1135 .header(CONTENT_TYPE, self.serialization.content_type());
1136
1137 if let Some(user_agent) = &self.custom_user_agent {
1138 rb = rb.header(USER_AGENT, user_agent.clone());
1139 }
1140
1141 if let Some(body) = body {
1142 match self.serialization {
1143 SerializationFormat::Json => {
1144 rb = rb.json(body);
1145 }
1146 SerializationFormat::Bincode => {
1147 let body = bincode::serialize(body)?;
1148 rb = rb.body(body);
1149 }
1150 SerializationFormat::Yaml => {
1151 let mut body_bytes = Vec::new();
1152 serde_yaml::to_writer(&mut body_bytes, &body)?;
1153 rb = rb.body(body_bytes);
1154 }
1155 SerializationFormat::Text => {
1156 let body = serde_plain::to_string(&body)?.as_bytes().to_vec();
1157 rb = rb.body(body);
1158 }
1159 }
1160 }
1161
1162 Ok(rb)
1163 }
1164
1165 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError> {
1166 let mut attempts = 0;
1167 loop {
1168 let r = request
1170 .try_clone()
1171 .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?;
1172
1173 let mut req = r
1176 .build()
1177 .map_err(HttpClientError::reqwest_client_build_error)?;
1178 self.apply_hosts_to_req(&mut req);
1179 let url: Url = req.url().clone().into();
1180
1181 let request_start = Instant::now();
1182
1183 #[cfg(target_arch = "wasm32")]
1184 let response: Result<Response, HttpClientError> = {
1185 let client = self
1186 .reqwest_client
1187 .as_ref()
1188 .unwrap_or_else(|| &*SHARED_CLIENT);
1189 Ok(
1190 wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
1191 .await
1192 .map_err(|_timeout| HttpClientError::RequestTimeout)??,
1193 )
1194 };
1195
1196 #[cfg(not(target_arch = "wasm32"))]
1197 let response = {
1198 let client = self
1199 .reqwest_client
1200 .as_ref()
1201 .unwrap_or_else(|| &*SHARED_CLIENT);
1202 client.execute(req).await
1203 };
1204
1205 match response {
1206 Ok(resp) => {
1207 if is_http_rate_limit_err(&resp) {
1209 warn!("encountered rate limit error for {}", url.as_str());
1210 self.maybe_rotate_hosts(Some(url.clone()));
1212 }
1213
1214 return Ok(resp);
1215 }
1216 Err(err) => {
1217 let last_network_reconfiguration =
1218 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap();
1219 let network_reconfigured =
1220 last_network_reconfiguration.is_some_and(|last| last > request_start);
1221
1222 #[cfg(target_arch = "wasm32")]
1223 let is_network_err = err.is_timeout();
1224 #[cfg(not(target_arch = "wasm32"))]
1225 let is_network_err = might_be_network_interference(&err);
1226
1227 if is_network_err & !network_reconfigured {
1228 self.maybe_rotate_hosts(Some(url.clone()));
1230
1231 #[cfg(feature = "tunneling")]
1232 self.maybe_enable_fronting(("network", url.as_str(), &err));
1233 }
1234
1235 if attempts < self.retry_limit {
1236 attempts += 1;
1237 warn!(
1238 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1239 self.retry_limit
1240 );
1241 continue;
1242 }
1243
1244 cfg_if::cfg_if! {
1246 if #[cfg(target_arch = "wasm32")] {
1247 return Err(err);
1248 } else {
1249 return Err(HttpClientError::request_send_error(url.into(), err));
1250 }
1251 }
1252 }
1253 }
1254 }
1255 }
1256
1257 fn maybe_rotate_hosts(&self, offending: Option<Url>) {
1258 self.update_host(offending);
1259 }
1260
1261 #[cfg(feature = "tunneling")]
1262 fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
1263 let was_enabled = self.front.is_enabled();
1266 self.front.retry_enable();
1267 if !was_enabled && self.front.is_enabled() {
1268 tracing::debug!("Domain fronting activated after failure: {context:?}",);
1269 }
1270 }
1271}
1272
1273pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
1276 is_rate_limit_response(resp.status(), resp.headers())
1277}
1278
1279fn is_rate_limit_response(status: StatusCode, headers: &HeaderMap) -> bool {
1280 let too_many_reqs = status == StatusCode::TOO_MANY_REQUESTS;
1281 let rate_limit_503 = is_throttled_service_unavailable(status, headers);
1282 let vercel_backoff = is_vercel_rate_limit_challenge(status, headers);
1283
1284 too_many_reqs || rate_limit_503 || vercel_backoff
1285}
1286
1287fn is_throttled_service_unavailable(status: StatusCode, headers: &HeaderMap) -> bool {
1296 status == StatusCode::SERVICE_UNAVAILABLE && headers.contains_key(RETRY_AFTER)
1297}
1298
1299const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
1300const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
1301
1302fn is_vercel_rate_limit_challenge(status: StatusCode, headers: &HeaderMap) -> bool {
1304 let status = status == StatusCode::FORBIDDEN;
1305 let header = headers
1306 .get(VERCEL_CHALLENGE_HEADER)
1307 .is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
1308 let content_type = headers
1309 .get(CONTENT_TYPE)
1310 .and_then(|value| value.to_str().ok())
1311 .and_then(|value| value.parse::<Mime>().ok())
1312 .is_some_and(|mime_type| {
1313 mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
1314 });
1315
1316 status && header && content_type
1317}
1318
1319#[cfg(not(target_arch = "wasm32"))]
1320const MAX_ERR_SOURCE_ITERATIONS: usize = 6;
1321
1322#[cfg(not(target_arch = "wasm32"))]
1331pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
1332 if err.is_timeout() {
1333 return true;
1334 }
1335
1336 if !(err.is_connect() || err.is_request()) {
1337 return false;
1338 }
1339
1340 let mut inner = err.source();
1345 for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
1346 if let Some(e) = inner {
1347 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
1348 match io_err.kind() {
1350 ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
1352 ErrorKind::ConnectionReset
1354 | ErrorKind::HostUnreachable
1355 | ErrorKind::ConnectionRefused => return true,
1356 ErrorKind::Other | ErrorKind::InvalidData => {
1358 inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
1361 }
1362 _ => return false,
1363 }
1364 } else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
1365 return true;
1367 } else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
1368 return resolve_err.is_nx_domain()
1372 || matches!(
1373 resolve_err,
1374 hickory_resolver::net::NetError::Dns(
1375 hickory_resolver::net::DnsError::ResponseCode(
1376 hickory_resolver::proto::op::ResponseCode::ServFail
1377 )
1378 )
1379 );
1380 } else if let Some(h2_err) = e.downcast_ref::<h2::Error>() {
1381 return h2_err.is_remote() && (h2_err.is_reset() || h2_err.is_go_away());
1390 } else {
1391 inner = e.source();
1392 }
1393 } else {
1394 break;
1395 }
1396 }
1397
1398 false
1399}
1400
1401#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1405#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1406pub trait ApiClient: ApiClientCore {
1407 fn create_get_request<P, K, V>(
1409 &self,
1410 path: P,
1411 params: Params<'_, K, V>,
1412 ) -> Result<RequestBuilder, HttpClientError>
1413 where
1414 P: RequestPath,
1415 K: AsRef<str>,
1416 V: AsRef<str>,
1417 {
1418 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1419 }
1420
1421 fn create_post_request<P, B, K, V>(
1423 &self,
1424 path: P,
1425 params: Params<'_, K, V>,
1426 json_body: &B,
1427 ) -> Result<RequestBuilder, HttpClientError>
1428 where
1429 P: RequestPath,
1430 B: Serialize + ?Sized,
1431 K: AsRef<str>,
1432 V: AsRef<str>,
1433 {
1434 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1435 }
1436
1437 fn create_delete_request<P, K, V>(
1439 &self,
1440 path: P,
1441 params: Params<'_, K, V>,
1442 ) -> Result<RequestBuilder, HttpClientError>
1443 where
1444 P: RequestPath,
1445 K: AsRef<str>,
1446 V: AsRef<str>,
1447 {
1448 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1449 }
1450
1451 fn create_patch_request<P, B, K, V>(
1453 &self,
1454 path: P,
1455 params: Params<'_, K, V>,
1456 json_body: &B,
1457 ) -> Result<RequestBuilder, HttpClientError>
1458 where
1459 P: RequestPath,
1460 B: Serialize + ?Sized,
1461 K: AsRef<str>,
1462 V: AsRef<str>,
1463 {
1464 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1465 }
1466
1467 #[instrument(level = "debug", skip_all, fields(path=?path))]
1469 async fn send_get_request<P, K, V>(
1470 &self,
1471 path: P,
1472 params: Params<'_, K, V>,
1473 ) -> Result<Response, HttpClientError>
1474 where
1475 P: RequestPath + Send + Sync,
1476 K: AsRef<str> + Sync,
1477 V: AsRef<str> + Sync,
1478 {
1479 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1480 .await
1481 }
1482
1483 async fn send_post_request<P, B, K, V>(
1485 &self,
1486 path: P,
1487 params: Params<'_, K, V>,
1488 json_body: &B,
1489 ) -> Result<Response, HttpClientError>
1490 where
1491 P: RequestPath + Send + Sync,
1492 B: Serialize + ?Sized + Sync,
1493 K: AsRef<str> + Sync,
1494 V: AsRef<str> + Sync,
1495 {
1496 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1497 .await
1498 }
1499
1500 async fn send_delete_request<P, K, V>(
1502 &self,
1503 path: P,
1504 params: Params<'_, K, V>,
1505 ) -> Result<Response, HttpClientError>
1506 where
1507 P: RequestPath + Send + Sync,
1508 K: AsRef<str> + Sync,
1509 V: AsRef<str> + Sync,
1510 {
1511 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1512 .await
1513 }
1514
1515 async fn send_patch_request<P, B, K, V>(
1517 &self,
1518 path: P,
1519 params: Params<'_, K, V>,
1520 json_body: &B,
1521 ) -> Result<Response, HttpClientError>
1522 where
1523 P: RequestPath + Send + Sync,
1524 B: Serialize + ?Sized + Sync,
1525 K: AsRef<str> + Sync,
1526 V: AsRef<str> + Sync,
1527 {
1528 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1529 .await
1530 }
1531
1532 #[instrument(level = "debug", skip_all, fields(path=?path))]
1536 async fn get_json<P, T, K, V>(
1538 &self,
1539 path: P,
1540 params: Params<'_, K, V>,
1541 ) -> Result<T, HttpClientError>
1542 where
1543 P: RequestPath + Send + Sync,
1544 for<'a> T: Deserialize<'a>,
1545 K: AsRef<str> + Sync,
1546 V: AsRef<str> + Sync,
1547 {
1548 self.get_response(path, params).await
1549 }
1550
1551 async fn parse_response<T>(
1553 &self,
1554 res: Response,
1555 allow_empty: bool,
1556 ) -> Result<T, HttpClientError>
1557 where
1558 T: DeserializeOwned,
1559 {
1560 let url = Url::from(res.url());
1561 parse_response(res, allow_empty).await.inspect_err(|e| {
1562 if matches!(
1563 e,
1566 HttpClientError::ResponseReadFailure {
1567 url: _,
1568 headers: _,
1569 status: _,
1570 source: _,
1571 }
1572 ) {
1573 self.maybe_rotate_hosts(Some(url.clone()));
1574 #[cfg(feature = "tunneling")]
1575 self.maybe_enable_fronting(("parse/read", url.as_str(), e));
1576 }
1577 })
1578 }
1579
1580 #[instrument(level = "debug", skip_all, fields(path=?path))]
1584 async fn get_response<P, T, K, V>(
1585 &self,
1586 path: P,
1587 params: Params<'_, K, V>,
1588 ) -> Result<T, HttpClientError>
1589 where
1590 P: RequestPath + Send + Sync,
1591 for<'a> T: Deserialize<'a>,
1592 K: AsRef<str> + Sync,
1593 V: AsRef<str> + Sync,
1594 {
1595 let res = self
1596 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1597 .await?;
1598
1599 self.parse_response(res, false).await
1600 }
1601
1602 async fn post_json<P, B, T, K, V>(
1606 &self,
1607 path: P,
1608 params: Params<'_, K, V>,
1609 json_body: &B,
1610 ) -> Result<T, HttpClientError>
1611 where
1612 P: RequestPath + Send + Sync,
1613 B: Serialize + ?Sized + Sync,
1614 for<'a> T: Deserialize<'a>,
1615 K: AsRef<str> + Sync,
1616 V: AsRef<str> + Sync,
1617 {
1618 let res = self
1619 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1620 .await?;
1621 self.parse_response(res, false).await
1622 }
1623
1624 async fn delete_json<P, T, K, V>(
1628 &self,
1629 path: P,
1630 params: Params<'_, K, V>,
1631 ) -> Result<T, HttpClientError>
1632 where
1633 P: RequestPath + Send + Sync,
1634 for<'a> T: Deserialize<'a>,
1635 K: AsRef<str> + Sync,
1636 V: AsRef<str> + Sync,
1637 {
1638 let res = self
1639 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1640 .await?;
1641 self.parse_response(res, false).await
1642 }
1643
1644 async fn patch_json<P, B, T, K, V>(
1648 &self,
1649 path: P,
1650 params: Params<'_, K, V>,
1651 json_body: &B,
1652 ) -> Result<T, HttpClientError>
1653 where
1654 P: RequestPath + Send + Sync,
1655 B: Serialize + ?Sized + Sync,
1656 for<'a> T: Deserialize<'a>,
1657 K: AsRef<str> + Sync,
1658 V: AsRef<str> + Sync,
1659 {
1660 let res = self
1661 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1662 .await?;
1663 self.parse_response(res, false).await
1664 }
1665
1666 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1669 where
1670 for<'a> T: Deserialize<'a>,
1671 S: AsRef<str> + Sync + Send,
1672 {
1673 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1674 let res = self.send(req).await?;
1675 self.parse_response(res, false).await
1676 }
1677
1678 async fn post_json_data_to<B, T, S>(
1681 &self,
1682 endpoint: S,
1683 json_body: &B,
1684 ) -> Result<T, HttpClientError>
1685 where
1686 B: Serialize + ?Sized + Sync,
1687 for<'a> T: Deserialize<'a>,
1688 S: AsRef<str> + Sync + Send,
1689 {
1690 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1691 let res = self.send(req).await?;
1692 self.parse_response(res, false).await
1693 }
1694
1695 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1698 where
1699 for<'a> T: Deserialize<'a>,
1700 S: AsRef<str> + Sync + Send,
1701 {
1702 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1703 let res = self.send(req).await?;
1704 self.parse_response(res, false).await
1705 }
1706
1707 async fn patch_json_data_at<B, T, S>(
1710 &self,
1711 endpoint: S,
1712 json_body: &B,
1713 ) -> Result<T, HttpClientError>
1714 where
1715 B: Serialize + ?Sized + Sync,
1716 for<'a> T: Deserialize<'a>,
1717 S: AsRef<str> + Sync + Send,
1718 {
1719 let req =
1720 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1721 let res = self.send(req).await?;
1722 self.parse_response(res, false).await
1723 }
1724}
1725
1726#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1727#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1728impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1729
1730fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1732 base: &Url,
1733 request_path: impl RequestPath,
1734 params: Params<'_, K, V>,
1735) -> Url {
1736 let mut url = base.clone();
1737 let mut path_segments = url
1738 .path_segments_mut()
1739 .expect("provided validator url does not have a base!");
1740
1741 path_segments.pop_if_empty();
1742
1743 for segment in request_path.to_sanitized_segments() {
1744 path_segments.push(segment);
1745 }
1746
1747 drop(path_segments);
1750
1751 if !params.is_empty() {
1752 url.query_pairs_mut().extend_pairs(params);
1753 }
1754
1755 url
1756}
1757
1758fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1759 use encoding_rs::{Encoding, UTF_8};
1760
1761 let content_type = try_get_mime_type(headers);
1762
1763 let encoding_name = content_type
1764 .as_ref()
1765 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1766 .unwrap_or("utf-8");
1767
1768 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1769
1770 let (text, _, _) = encoding.decode(bytes);
1771 text.into_owned()
1772}
1773
1774#[instrument(level = "debug", skip_all)]
1776pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1777where
1778 T: DeserializeOwned,
1779{
1780 let status = res.status();
1781 let headers = res.headers().clone();
1782 let url = res.url().clone();
1783
1784 tracing::trace!("status: {status} (success: {})", status.is_success());
1785 tracing::trace!("headers: {headers:?}");
1786
1787 if !allow_empty && let Some(0) = res.content_length() {
1788 return Err(HttpClientError::EmptyResponse {
1789 url: Box::new(url),
1790 status,
1791 headers: Box::new(headers),
1792 });
1793 }
1794
1795 if res.status().is_success() {
1796 let full = res
1799 .bytes()
1800 .await
1801 .map_err(|source| HttpClientError::ResponseReadFailure {
1802 url: Box::new(url),
1803 headers: Box::new(headers.clone()),
1804 status,
1805 source: ReqwestErrorWrapper(source),
1806 })?;
1807 decode_raw_response(&headers, full)
1808 } else if res.status() == StatusCode::NOT_FOUND {
1809 Err(HttpClientError::NotFound { url: Box::new(url) })
1810 } else if is_http_rate_limit_err(&res) {
1811 Err(HttpClientError::EndpointFailure {
1812 url: Box::new(url),
1813 status,
1814 headers: Box::new(headers),
1815 error: String::from("received rate limit response"),
1816 })
1817 } else {
1818 let Ok(plaintext) = res.text().await else {
1819 return Err(HttpClientError::RequestFailure {
1820 url: Box::new(url),
1821 status,
1822 headers: Box::new(headers),
1823 });
1824 };
1825
1826 Err(HttpClientError::EndpointFailure {
1827 url: Box::new(url),
1828 status,
1829 headers: Box::new(headers),
1830 error: plaintext,
1831 })
1832 }
1833}
1834
1835fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1836where
1837 T: DeserializeOwned,
1838{
1839 match serde_json::from_slice(&content) {
1840 Ok(data) => Ok(data),
1841 Err(err) => {
1842 let content = decode_as_text(&content, headers);
1843 Err(HttpClientError::ResponseDecodeFailure {
1844 message: err.to_string(),
1845 content,
1846 })
1847 }
1848 }
1849}
1850
1851fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1852where
1853 T: DeserializeOwned,
1854{
1855 use bincode::Options;
1856
1857 let opts = nym_http_api_common::make_bincode_serializer();
1858 match opts.deserialize(&content) {
1859 Ok(data) => Ok(data),
1860 Err(err) => {
1861 let content = decode_as_text(&content, headers);
1862 Err(HttpClientError::ResponseDecodeFailure {
1863 message: err.to_string(),
1864 content,
1865 })
1866 }
1867 }
1868}
1869
1870fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1871where
1872 T: DeserializeOwned,
1873{
1874 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
1876
1877 debug!("attempting to parse response as {mime}");
1878
1879 match (mime.type_(), mime.subtype().as_str()) {
1881 (mime::APPLICATION, "json") => decode_as_json(headers, content),
1882 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
1883 (_, _) => {
1884 debug!("unrecognised mime type {mime}. falling back to json decoding...");
1885 decode_as_json(headers, content)
1886 }
1887 }
1888}
1889
1890fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
1891 headers
1892 .get(CONTENT_TYPE)
1893 .and_then(|value| value.to_str().ok())
1894 .and_then(|value| value.parse::<Mime>().ok())
1895}
1896
1897#[cfg(test)]
1898mod tests;