1#![allow(deprecated)]
5#![warn(missing_docs)]
141
142pub use inventory;
143pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
144use std::error::Error;
145use std::sync::Mutex;
146#[cfg(not(target_arch = "wasm32"))]
147use std::time::Instant;
148#[cfg(target_arch = "wasm32")]
149use wasmtimer::std::Instant;
150
151pub mod registry;
152
153pub use crate::path::RequestPath;
157use async_trait::async_trait;
158use bytes::Bytes;
159use cfg_if::cfg_if;
160use http::{
161 HeaderMap,
162 header::{ACCEPT, CONTENT_TYPE, RETRY_AFTER, USER_AGENT},
163};
164use itertools::Itertools;
165use mime::Mime;
166use reqwest::{RequestBuilder, Response, header::HeaderValue};
167use serde::{Deserialize, Serialize, de::DeserializeOwned};
168#[cfg(not(target_arch = "wasm32"))]
169use std::io::ErrorKind;
170use std::{
171 fmt::Display,
172 sync::atomic::{AtomicUsize, Ordering},
173 time::Duration,
174};
175use thiserror::Error;
176use tracing::{debug, instrument, warn};
177
178use std::sync::{Arc, LazyLock};
179
180#[cfg(feature = "tunneling")]
181mod fronted;
182#[cfg(feature = "tunneling")]
183pub use fronted::{FrontPolicy, FrontingConfig};
184#[cfg(feature = "tunneling")]
185mod rotation;
186#[cfg(feature = "tunneling")]
187use rotation::RotationManager;
188mod host_header;
189mod url;
190pub use url::{IntoUrl, Url};
191mod user_agent;
192pub use user_agent::UserAgent;
193
194#[cfg(not(target_arch = "wasm32"))]
195pub mod dns;
196mod path;
197#[cfg(not(target_arch = "wasm32"))]
198pub mod tls;
199
200#[cfg(not(target_arch = "wasm32"))]
201pub use dns::{HickoryDnsResolver, ResolveError};
202
203#[cfg(not(target_arch = "wasm32"))]
205use crate::registry::default_builder;
206#[doc(hidden)]
207pub use nym_bin_common::bin_info;
208#[cfg(not(target_arch = "wasm32"))]
209use nym_http_api_client_macro::client_defaults;
210
211pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
216
217const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
218
219#[cfg(not(target_arch = "wasm32"))]
220client_defaults!(
221 priority = -100;
222 gzip = true,
223 deflate = true,
224 brotli = true,
225 zstd = true,
226 timeout = DEFAULT_TIMEOUT,
227 user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
228);
229
230static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
231 tracing::info!("Initializing shared HTTP client");
232 cfg_if! {
233 if #[cfg(target_arch = "wasm32")] {
234 reqwest::ClientBuilder::new().build()
235 .expect("failed to initialize shared http client")
236 } else {
237 let mut builder = default_builder();
238
239 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::new()));
240
241 builder
242 .build()
243 .expect("failed to initialize shared http client")
244 }
245 }
246});
247
248pub(crate) static SHARED_NETWORK_RECONFIGURATION: LazyLock<Arc<Mutex<Option<Instant>>>> =
249 LazyLock::new(|| Arc::new(Mutex::new(None)));
250
251pub fn network_reconfigured() {
253 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap() = Some(Instant::now());
254}
255
256pub type PathSegments<'a> = &'a [&'a str];
258pub type Params<'a, K, V> = &'a [(K, V)];
260
261pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[];
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum SerializationFormat {
267 Json,
269 Bincode,
271 Yaml,
273 Text,
275}
276
277impl Display for SerializationFormat {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 match self {
280 SerializationFormat::Json => write!(f, "json"),
281 SerializationFormat::Bincode => write!(f, "bincode"),
282 SerializationFormat::Yaml => write!(f, "yaml"),
283 SerializationFormat::Text => write!(f, "text"),
284 }
285 }
286}
287
288impl SerializationFormat {
289 #[allow(missing_docs)]
290 pub fn content_type(&self) -> String {
291 match self {
292 SerializationFormat::Json => "application/json".to_string(),
293 SerializationFormat::Bincode => "application/bincode".to_string(),
294 SerializationFormat::Yaml => "application/yaml".to_string(),
295 SerializationFormat::Text => "text/plain".to_string(),
296 }
297 }
298}
299
300#[allow(missing_docs)]
301#[derive(Debug)]
302pub struct ReqwestErrorWrapper(reqwest::Error);
303
304impl Display for ReqwestErrorWrapper {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 cfg_if::cfg_if! {
307 if #[cfg(not(target_arch = "wasm32"))] {
308 if self.0.is_connect() {
309 write!(f, "failed to connect: ")?;
310 }
311 }
312 }
313
314 if self.0.is_timeout() {
315 write!(f, "timed out: ")?;
316 }
317 if self.0.is_redirect()
318 && let Some(final_stop) = self.0.url()
319 {
320 write!(f, "redirect loop at {final_stop}: ")?;
321 }
322
323 self.0.fmt(f)?;
324 if let Some(status_code) = self.0.status() {
325 write!(f, " status: {status_code}")?;
326 } else {
327 write!(f, " unknown status code")?;
328 }
329
330 if let Some(source) = self.0.source() {
331 write!(f, " source: {source:?}")?;
332 } else {
333 write!(f, " unknown lower-level error source")?;
334 }
335
336 Ok(())
337 }
338}
339
340impl std::error::Error for ReqwestErrorWrapper {}
341
342#[derive(Debug, Error)]
344#[allow(missing_docs)]
345pub enum HttpClientError {
346 #[error("did not provide any valid client URLs")]
347 NoUrlsProvided,
348
349 #[error("failed to construct inner reqwest client: {source}")]
350 ReqwestBuildError {
351 #[source]
352 source: reqwest::Error,
353 },
354
355 #[deprecated(
356 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
357 )]
358 #[error("request failed with error message: {0}")]
359 GenericRequestFailure(String),
360
361 #[deprecated(
362 note = "use another more strongly typed variant - this variant is only left for compatibility reasons"
363 )]
364 #[error("there was an issue with the REST request: {source}")]
365 ReqwestClientError {
366 #[from]
367 source: reqwest::Error,
368 },
369
370 #[error("failed to parse {raw} as a valid URL: {source}")]
371 MalformedUrl {
372 raw: String,
373 #[source]
374 source: reqwest::Error,
375 },
376
377 #[error("failed to parse header value: {source}")]
378 InvalidHeaderValue {
379 #[source]
380 source: http::Error,
381 },
382
383 #[error("failed to send request for {url}: {source}")]
384 RequestSendFailure {
385 url: Box<reqwest::Url>,
386 #[source]
387 source: ReqwestErrorWrapper,
388 },
389
390 #[error("failed to read response body from {url}: {source}")]
391 ResponseReadFailure {
392 url: Box<reqwest::Url>,
393 headers: Box<HeaderMap>,
394 status: StatusCode,
395 #[source]
396 source: ReqwestErrorWrapper,
397 },
398
399 #[error("failed to deserialize received response: {source}")]
400 ResponseDeserialisationFailure { source: serde_json::Error },
401
402 #[error("provided url is malformed: {source}")]
403 UrlParseFailure {
404 #[from]
405 source: url::ParseError,
406 },
407
408 #[error("the requested resource could not be found at {url}")]
409 NotFound { url: Box<reqwest::Url> },
410
411 #[error("attempted to use domain fronting and clone a request containing stream data")]
412 AttemptedToCloneStreamRequest,
413
414 #[error(
418 "the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
419 )]
420 RequestFailure {
421 url: Box<reqwest::Url>,
422 status: StatusCode,
423 headers: Box<HeaderMap>,
424 },
425
426 #[error(
427 "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
428 )]
429 EmptyResponse {
430 url: Box<reqwest::Url>,
431 status: StatusCode,
432 headers: Box<HeaderMap>,
433 },
434
435 #[error(
436 "failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
437 )]
438 EndpointFailure {
439 url: Box<reqwest::Url>,
440 status: StatusCode,
441 headers: Box<HeaderMap>,
442 error: String,
443 },
444
445 #[error("failed to decode response body: {message} from {content}")]
446 ResponseDecodeFailure { message: String, content: String },
447
448 #[error("failed to resolve request to {url} due to data inconsistency: {details}")]
449 InternalResponseInconsistency { url: ::url::Url, details: String },
450
451 #[cfg(not(target_arch = "wasm32"))]
452 #[error("encountered dns failure: {inner}")]
453 DnsLookupFailure {
454 #[from]
455 inner: ResolveError,
456 },
457
458 #[error("Failed to encode bincode: {0}")]
459 Bincode(#[from] bincode::Error),
460
461 #[error("Failed to json: {0}")]
462 Json(#[from] serde_json::Error),
463
464 #[error("Failed to yaml: {0}")]
465 Yaml(#[from] serde_yaml::Error),
466
467 #[error("Failed to plain: {0}")]
468 Plain(#[from] serde_plain::Error),
469
470 #[cfg(target_arch = "wasm32")]
471 #[error("the request has timed out")]
472 RequestTimeout,
473}
474
475#[allow(missing_docs)]
476#[allow(deprecated)]
477impl HttpClientError {
478 pub fn is_timeout(&self) -> bool {
480 match self {
481 HttpClientError::ReqwestClientError { source } => source.is_timeout(),
482 HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
483 HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
484 #[cfg(not(target_arch = "wasm32"))]
485 HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
486 #[cfg(target_arch = "wasm32")]
487 HttpClientError::RequestTimeout => true,
488 _ => false,
489 }
490 }
491
492 pub fn status_code(&self) -> Option<StatusCode> {
494 match self {
495 HttpClientError::ResponseReadFailure { status, .. } => Some(*status),
496 HttpClientError::RequestFailure { status, .. } => Some(*status),
497 HttpClientError::EmptyResponse { status, .. } => Some(*status),
498 HttpClientError::EndpointFailure { status, .. } => Some(*status),
499 _ => None,
500 }
501 }
502
503 pub fn reqwest_client_build_error(source: reqwest::Error) -> Self {
504 HttpClientError::ReqwestBuildError { source }
505 }
506
507 pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
508 HttpClientError::RequestSendFailure {
509 url: Box::new(url),
510 source: ReqwestErrorWrapper(source),
511 }
512 }
513
514 pub fn is_data_inconsistency(&self) -> bool {
515 matches!(self, HttpClientError::InternalResponseInconsistency { .. })
516 }
517}
518
519#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
525#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
526pub trait ApiClientCore {
527 fn create_request<P, B, K, V>(
529 &self,
530 method: reqwest::Method,
531 path: P,
532 params: Params<'_, K, V>,
533 body: Option<&B>,
534 ) -> Result<RequestBuilder, HttpClientError>
535 where
536 P: RequestPath,
537 B: Serialize + ?Sized,
538 K: AsRef<str>,
539 V: AsRef<str>;
540
541 fn create_request_endpoint<B, S>(
557 &self,
558 method: reqwest::Method,
559 endpoint: S,
560 body: Option<&B>,
561 ) -> Result<RequestBuilder, HttpClientError>
562 where
563 B: Serialize + ?Sized,
564 S: AsRef<str>,
565 {
566 let mut standin_url: Url = "http://example.com".parse().unwrap();
571
572 match endpoint.as_ref().split_once("?") {
573 Some((path, query)) => {
574 standin_url.set_path(path);
575 standin_url.set_query(Some(query));
576 }
577 None => standin_url.set_path(endpoint.as_ref()),
579 }
580
581 let path: Vec<&str> = match standin_url.path_segments() {
582 Some(segments) => segments.collect(),
583 None => Vec::new(),
584 };
585 let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
586
587 self.create_request(method, path.as_slice(), ¶ms, body)
588 }
589
590 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError>;
596
597 async fn send_request<P, B, K, V>(
599 &self,
600 method: reqwest::Method,
601 path: P,
602 params: Params<'_, K, V>,
603 json_body: Option<&B>,
604 ) -> Result<Response, HttpClientError>
605 where
606 P: RequestPath + Send + Sync,
607 B: Serialize + ?Sized + Sync,
608 K: AsRef<str> + Sync,
609 V: AsRef<str> + Sync,
610 {
611 let req = self.create_request(method, path, params, json_body)?;
612 self.send(req).await
613 }
614
615 fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
622
623 #[cfg(feature = "tunneling")]
628 fn maybe_enable_fronting(&self, domain: Option<&str>, context: impl std::fmt::Debug);
629}
630
631pub struct ClientBuilder {
634 urls: Vec<Url>,
635
636 timeout: Option<Duration>,
637 custom_user_agent: Option<HeaderValue>,
638 reqwest_client_builder: Option<reqwest::ClientBuilder>,
639 #[allow(dead_code)] use_secure_dns: bool,
641
642 #[cfg(feature = "tunneling")]
643 front: fronted::Front,
644
645 retry_limit: usize,
646 serialization: SerializationFormat,
647
648 error: Option<HttpClientError>,
649}
650
651impl ClientBuilder {
652 pub fn new<U>(url: U) -> Result<Self, HttpClientError>
656 where
657 U: IntoUrl,
658 {
659 let str_url = url.as_str();
660
661 if !str_url.starts_with("http") {
663 let alt = format!("http://{str_url}");
664 warn!(
665 "the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."
666 );
667 Self::new(alt)
669 } else {
670 let url = url.to_url()?;
671 Self::new_with_urls(vec![url])
672 }
673 }
674
675 #[cfg(feature = "network-defaults")]
677 #[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
680 pub fn from_network(
681 network: &nym_network_defaults::v2::NymNetworkDetails,
682 ) -> Result<Self, HttpClientError> {
683 let urls = network.nym_api_urls();
684 Self::new_with_fronted_urls(urls.clone())
685 }
686
687 #[cfg(feature = "network-defaults")]
689 pub fn new_with_fronted_urls(
690 urls: Vec<nym_network_defaults::ApiUrl>,
691 ) -> Result<Self, HttpClientError> {
692 let urls = urls
693 .into_iter()
694 .map(|api_url| {
695 let mut url = Url::parse(&api_url.url)?;
697
698 #[cfg(feature = "tunneling")]
700 if let Some(ref front_hosts) = api_url.front_hosts {
701 let fronts: Vec<String> = front_hosts
702 .iter()
703 .map(|host| format!("https://{}", host))
704 .collect();
705 url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
706 HttpClientError::MalformedUrl {
707 raw: api_url.url.clone(),
708 source,
709 }
710 })?;
711 }
712
713 Ok(url)
714 })
715 .collect::<Result<Vec<_>, HttpClientError>>()?;
716
717 let mut builder = Self::new_with_urls(urls)?;
718
719 #[cfg(feature = "tunneling")]
721 {
722 builder = builder.with_fronting(None);
723 }
724
725 Ok(builder)
726 }
727
728 pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
730 if urls.is_empty() {
731 return Err(HttpClientError::NoUrlsProvided);
732 }
733
734 let urls = Self::check_urls(urls);
735
736 Ok(ClientBuilder {
737 urls,
738 timeout: None,
739 custom_user_agent: None,
740 reqwest_client_builder: None,
741 use_secure_dns: true,
742 #[cfg(feature = "tunneling")]
743 front: fronted::Front::off(),
744
745 retry_limit: 0,
746 serialization: SerializationFormat::Json,
747 error: None,
748 })
749 }
750
751 #[cfg(not(target_arch = "wasm32"))]
754 pub fn non_shared(mut self) -> Self {
755 if self.reqwest_client_builder.is_none() {
756 self.reqwest_client_builder = Some(default_builder());
757 }
758 self
759 }
760
761 pub fn add_url(mut self, url: Url) -> Self {
763 self.urls.push(url);
764 self
765 }
766
767 fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
768 urls = urls.into_iter().unique().collect();
770
771 urls.iter()
773 .filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
774 .for_each(|url| {
775 warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
776 });
777
778 urls
779 }
780
781 #[must_use]
787 pub fn with_timeout(mut self, timeout: Duration) -> Self {
788 self.timeout = Some(timeout);
789 self
790 }
791
792 #[must_use]
800 pub fn with_retries(mut self, retry_limit: usize) -> Self {
801 self.retry_limit = retry_limit;
802 self
803 }
804
805 #[must_use]
807 pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
808 self.reqwest_client_builder = Some(reqwest_builder);
809 self
810 }
811
812 #[must_use]
814 pub fn with_user_agent<V>(mut self, value: V) -> Self
815 where
816 V: TryInto<HeaderValue>,
817 V::Error: Into<http::Error>,
818 {
819 match value.try_into() {
820 Ok(v) => self.custom_user_agent = Some(v),
821 Err(err) => {
822 self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
823 }
824 }
825 self
826 }
827
828 #[must_use]
830 pub fn with_serialization(mut self, format: SerializationFormat) -> Self {
831 self.serialization = format;
832 self
833 }
834
835 #[must_use]
837 pub fn with_bincode(self) -> Self {
838 self.with_serialization(SerializationFormat::Bincode)
839 }
840
841 pub fn build(self) -> Result<Client, HttpClientError> {
843 if let Some(err) = self.error {
844 return Err(err);
845 }
846
847 #[cfg(target_arch = "wasm32")]
848 let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
849
850 #[cfg(not(target_arch = "wasm32"))]
851 let reqwest_client = self
852 .reqwest_client_builder
853 .map(|mut builder| {
854 if self.use_secure_dns {
856 builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::new()));
857 }
858
859 tls::install_default_crypto_provider();
861 builder
862 .build()
863 .map_err(HttpClientError::reqwest_client_build_error)
864 })
865 .transpose()?;
866
867 #[cfg(feature = "tunneling")]
868 let rotation = RotationManager::new(self.urls.len());
869
870 let client = Client {
871 base_urls: self.urls,
872 current_idx: Arc::new(AtomicUsize::new(0)),
873 reqwest_client,
874 custom_user_agent: self.custom_user_agent,
875
876 #[cfg(feature = "tunneling")]
877 front: self.front,
878 #[cfg(feature = "tunneling")]
879 rotation,
880
881 #[cfg(target_arch = "wasm32")]
882 request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
883 retry_limit: self.retry_limit,
884 serialization: self.serialization,
885 };
886
887 Ok(client)
888 }
889}
890
891#[derive(Debug, Clone)]
893pub struct Client {
894 base_urls: Vec<Url>,
895 current_idx: Arc<AtomicUsize>,
896 reqwest_client: Option<reqwest::Client>,
897 custom_user_agent: Option<HeaderValue>,
898
899 #[cfg(feature = "tunneling")]
900 front: fronted::Front,
901 #[cfg(feature = "tunneling")]
904 rotation: RotationManager,
905
906 #[cfg(target_arch = "wasm32")]
907 request_timeout: Duration,
908
909 retry_limit: usize,
910 serialization: SerializationFormat,
911}
912
913impl Client {
914 pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
920 Self::new_url(base_url, timeout).expect(
921 "we provided valid url and we were unwrapping previous construction errors anyway",
922 )
923 }
924
925 pub fn new_url<U>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError>
927 where
928 U: IntoUrl,
929 {
930 let builder = Self::builder(url)?;
931 match timeout {
932 Some(timeout) => builder.with_timeout(timeout).build(),
933 None => builder.build(),
934 }
935 }
936
937 pub fn builder<U>(url: U) -> Result<ClientBuilder, HttpClientError>
941 where
942 U: IntoUrl,
943 {
944 ClientBuilder::new(url)
945 }
946
947 pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
949 self.current_idx.store(0, Ordering::Relaxed);
950 #[cfg(feature = "tunneling")]
951 {
952 self.rotation = RotationManager::new(new_urls.len());
953 self.front.reset_failures();
955 }
956 self.base_urls = new_urls
957 }
958
959 pub fn clone_with_new_url(&self, new_url: Url) -> Self {
961 Client {
962 base_urls: vec![new_url],
963 current_idx: Arc::new(Default::default()),
964 reqwest_client: None,
965 custom_user_agent: None,
966
967 #[cfg(feature = "tunneling")]
968 front: self.front.clone(),
969 #[cfg(feature = "tunneling")]
970 rotation: RotationManager::new(1),
971 retry_limit: self.retry_limit,
972
973 #[cfg(target_arch = "wasm32")]
974 request_timeout: self.request_timeout,
975 serialization: self.serialization,
976 }
977 }
978
979 pub fn current_url(&self) -> &Url {
981 &self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
982 }
983
984 #[cfg(feature = "tunneling")]
988 pub fn current_url_str(&self) -> &str {
989 let idx = self.current_idx.load(Ordering::Relaxed);
990 let url = &self.base_urls[idx];
991 if self.front.is_enabled() {
992 self.rotation
993 .as_str(idx, url, self.front.include_non_fronted_in_rotation())
994 } else {
995 url.inner_url().as_str()
996 }
997 }
998
999 #[cfg(feature = "tunneling")]
1002 pub fn current_front_host(&self) -> Option<&str> {
1003 let idx = self.current_idx.load(Ordering::Relaxed);
1004 self.dynamic_front_str(idx, &self.base_urls[idx])
1005 }
1006
1007 pub fn base_urls(&self) -> &[Url] {
1009 &self.base_urls
1010 }
1011
1012 pub fn base_urls_mut(&mut self) -> &mut [Url] {
1014 &mut self.base_urls
1015 }
1016
1017 pub fn change_retry_limit(&mut self, limit: usize) {
1019 self.retry_limit = limit;
1020 }
1021
1022 #[cfg(feature = "tunneling")]
1036 fn dynamic_front_str<'a>(&self, idx: usize, url: &'a Url) -> Option<&'a str> {
1037 if !self.front.is_enabled() {
1038 return None;
1039 }
1040 if self.front.include_non_fronted_in_rotation() {
1041 self.rotation.active_rotation_front_str(idx, url)
1042 } else {
1043 self.rotation.front_str(idx, url)
1044 }
1045 }
1046
1047 #[cfg(feature = "tunneling")]
1048 fn matches_current_host(&self, url: &Url) -> bool {
1049 let idx = self.current_idx.load(Ordering::Relaxed);
1050 let current = &self.base_urls[idx];
1051
1052 if self.front.is_enabled() && current.has_front() {
1055 let active_front = self.dynamic_front_str(idx, current);
1056
1057 match active_front {
1058 Some(_) => url.host_str() == active_front,
1059 None => url.host_str() == current.host_str(),
1060 }
1061 } else {
1062 url.host_str() == current.host_str()
1063 }
1064 }
1065
1066 #[cfg(not(feature = "tunneling"))]
1067 fn matches_current_host(&self, url: &Url) -> bool {
1068 url.host_str() == self.current_url().host_str()
1069 }
1070
1071 fn update_host(&self, maybe_url: Option<Url>) {
1078 if let Some(err_url) = maybe_url
1080 && !self.matches_current_host(&err_url)
1081 {
1082 return;
1083 }
1084
1085 #[cfg(feature = "tunneling")]
1086 if self.front.is_enabled() {
1087 let idx = self.current_idx.load(Ordering::Relaxed);
1088 let url = &self.base_urls[idx];
1089
1090 if self.front.include_non_fronted_in_rotation() {
1091 if url.has_front() && self.rotation.take_rotation_turn(idx, url) {
1094 return;
1095 }
1096 } else if url.has_front() && !self.rotation.update(idx, url) {
1097 return;
1101 }
1102 }
1103
1104 if self.base_urls.len() > 1 {
1105 let orig = self.current_idx.load(Ordering::Relaxed);
1106
1107 #[allow(unused_mut)]
1108 let mut next = (orig + 1) % self.base_urls.len();
1109
1110 #[cfg(feature = "tunneling")]
1113 if self.front.is_enabled() && !self.front.include_non_fronted_in_rotation() {
1114 while next != orig {
1115 if self.base_urls[next].has_front() {
1116 break;
1118 }
1119
1120 next = (next + 1) % self.base_urls.len();
1121 }
1122 }
1123
1124 self.current_idx.store(next, Ordering::Relaxed);
1125 debug!(
1126 "http client rotating host {} -> {}",
1127 self.base_urls[orig], self.base_urls[next]
1128 );
1129 }
1130 }
1131
1132 pub(crate) fn apply_hosts_to_req(
1145 &self,
1146 r: &mut reqwest::Request,
1147 ) -> (Option<&str>, Option<&str>) {
1148 let idx = self.current_idx.load(Ordering::Relaxed);
1152 let url = &self.base_urls[idx];
1153 let domain = url.host_str();
1154 r.url_mut().set_host(domain).unwrap();
1155
1156 let caller_overrode_host = host_header::overridden_by_caller(r);
1160
1161 #[cfg(feature = "tunneling")]
1162 if self.front.is_enabled() {
1163 let front_host = self.dynamic_front_str(idx, url);
1164
1165 if let Some(front_host) = front_host {
1166 if let Some(actual_host) = url.host_str() {
1167 tracing::debug!(
1168 "Domain fronting enabled: routing via CDN {} to actual host {}",
1169 front_host,
1170 actual_host
1171 );
1172
1173 r.url_mut().set_host(Some(front_host)).unwrap();
1175
1176 if caller_overrode_host {
1177 warn!(
1178 front = front_host,
1179 actual_host,
1180 "caller supplied a Host header on a fronted request; domain fronting \
1181 relies on the Host header matching the actual backend host, so this \
1182 request will likely fail to route through the CDN",
1183 );
1184 host_header::collapse_override(r);
1185 } else {
1186 host_header::set(r, Some(actual_host));
1187 }
1188
1189 let front_host_header: HeaderValue =
1191 front_host.parse().unwrap_or(HeaderValue::from_static(""));
1192 _ = r
1193 .headers_mut()
1194 .insert(NYM_OUTER_SNI_HEADER, front_host_header);
1195
1196 return (domain, Some(front_host));
1197 } else {
1198 tracing::debug!(
1199 "Domain fronting is enabled, but no host_url is defined for current URL"
1200 )
1201 }
1202 } else {
1203 tracing::debug!(
1204 "Domain fronting is enabled, but current URL has no front_hosts configured"
1205 )
1206 }
1207 }
1208
1209 if caller_overrode_host {
1213 host_header::collapse_override(r);
1214 } else {
1215 host_header::set(r, None);
1216 }
1217 r.headers_mut().remove(NYM_OUTER_SNI_HEADER);
1218
1219 (domain, None)
1220 }
1221}
1222
1223#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1224#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1225impl ApiClientCore for Client {
1226 #[instrument(level = "debug", skip_all, fields(path=?path))]
1227 fn create_request<P, B, K, V>(
1228 &self,
1229 method: reqwest::Method,
1230 path: P,
1231 params: Params<'_, K, V>,
1232 body: Option<&B>,
1233 ) -> Result<RequestBuilder, HttpClientError>
1234 where
1235 P: RequestPath,
1236 B: Serialize + ?Sized,
1237 K: AsRef<str>,
1238 V: AsRef<str>,
1239 {
1240 let url = self.current_url();
1241 let url = sanitize_url(url, path, params);
1242
1243 let mut req = reqwest::Request::new(method, url.into());
1244
1245 self.apply_hosts_to_req(&mut req);
1246
1247 let client = if let Some(client) = &self.reqwest_client {
1248 client.clone()
1249 } else {
1250 SHARED_CLIENT.clone()
1251 };
1252 let mut rb = RequestBuilder::from_parts(client, req);
1253
1254 rb = rb
1255 .header(ACCEPT, self.serialization.content_type())
1256 .header(CONTENT_TYPE, self.serialization.content_type());
1257
1258 if let Some(user_agent) = &self.custom_user_agent {
1259 rb = rb.header(USER_AGENT, user_agent.clone());
1260 }
1261
1262 if let Some(body) = body {
1263 match self.serialization {
1264 SerializationFormat::Json => {
1265 rb = rb.json(body);
1266 }
1267 SerializationFormat::Bincode => {
1268 let body = bincode::serialize(body)?;
1269 rb = rb.body(body);
1270 }
1271 SerializationFormat::Yaml => {
1272 let mut body_bytes = Vec::new();
1273 serde_yaml::to_writer(&mut body_bytes, &body)?;
1274 rb = rb.body(body_bytes);
1275 }
1276 SerializationFormat::Text => {
1277 let body = serde_plain::to_string(&body)?.as_bytes().to_vec();
1278 rb = rb.body(body);
1279 }
1280 }
1281 }
1282
1283 Ok(rb)
1284 }
1285
1286 async fn send(&self, request: RequestBuilder) -> Result<Response, HttpClientError> {
1287 let mut attempts = 0;
1288 loop {
1289 let r = request
1291 .try_clone()
1292 .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?;
1293
1294 let mut req = r
1297 .build()
1298 .map_err(HttpClientError::reqwest_client_build_error)?;
1299 let (domain, _front_used) = self.apply_hosts_to_req(&mut req);
1300 let domain = domain.map(str::to_owned);
1301 let url: Url = req.url().clone().into();
1302 req.headers_mut().remove(host_header::MANAGED_MARKER_HEADER);
1304
1305 let request_start = Instant::now();
1306
1307 #[cfg(target_arch = "wasm32")]
1308 let response: Result<Response, HttpClientError> = {
1309 let client = self
1310 .reqwest_client
1311 .as_ref()
1312 .unwrap_or_else(|| &*SHARED_CLIENT);
1313 Ok(
1314 wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
1315 .await
1316 .map_err(|_timeout| HttpClientError::RequestTimeout)??,
1317 )
1318 };
1319
1320 #[cfg(not(target_arch = "wasm32"))]
1321 let response = {
1322 let client = self
1323 .reqwest_client
1324 .as_ref()
1325 .unwrap_or_else(|| &*SHARED_CLIENT);
1326 client.execute(req).await
1327 };
1328
1329 match response {
1330 Ok(resp) => {
1331 if is_http_rate_limit_err(&resp) {
1333 warn!("encountered rate limit error for {}", url.as_str());
1334 self.maybe_rotate_hosts(Some(url.clone()));
1336 } else {
1337 #[cfg(feature = "tunneling")]
1346 if _front_used.is_none()
1347 && self.front.is_enabled()
1348 && self.front.should_recover_on_non_fronted_success()
1349 && resp.status().is_success()
1350 {
1351 debug!(
1352 "non-fronted request to {} succeeded while fronting was enabled; disabling fronting and resetting retry counters",
1353 url.as_str()
1354 );
1355 self.front.recover();
1356 }
1357 }
1358
1359 return Ok(resp);
1360 }
1361 Err(err) => {
1362 let last_network_reconfiguration =
1363 *SHARED_NETWORK_RECONFIGURATION.lock().unwrap();
1364 let network_reconfigured =
1365 last_network_reconfiguration.is_some_and(|last| last > request_start);
1366
1367 #[cfg(target_arch = "wasm32")]
1368 let is_network_err = err.is_timeout();
1369 #[cfg(not(target_arch = "wasm32"))]
1370 let is_network_err = might_be_network_interference(&err);
1371
1372 if is_network_err & !network_reconfigured {
1373 self.maybe_rotate_hosts(Some(url.clone()));
1375
1376 #[cfg(feature = "tunneling")]
1377 self.maybe_enable_fronting(
1378 domain.as_deref(),
1379 ("network", url.as_str(), &err),
1380 );
1381 }
1382
1383 if attempts < self.retry_limit {
1384 attempts += 1;
1385 warn!(
1386 "Retrying request due to http error on attempt ({attempts}/{}): {err}",
1387 self.retry_limit
1388 );
1389 continue;
1390 }
1391
1392 cfg_if::cfg_if! {
1394 if #[cfg(target_arch = "wasm32")] {
1395 return Err(err);
1396 } else {
1397 return Err(HttpClientError::request_send_error(url.into(), err));
1398 }
1399 }
1400 }
1401 }
1402 }
1403 }
1404
1405 fn maybe_rotate_hosts(&self, offending: Option<Url>) {
1406 self.update_host(offending);
1407 }
1408
1409 #[cfg(feature = "tunneling")]
1410 fn maybe_enable_fronting(&self, domain: Option<&str>, context: impl std::fmt::Debug) {
1411 let was_enabled = self.front.is_enabled();
1414 self.front.retry_enable(domain);
1415 if !was_enabled && self.front.is_enabled() {
1416 tracing::debug!("Domain fronting activated after failure: {context:?}",);
1417 }
1418 }
1419}
1420
1421pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
1424 is_rate_limit_response(resp.status(), resp.headers())
1425}
1426
1427fn is_rate_limit_response(status: StatusCode, headers: &HeaderMap) -> bool {
1428 let too_many_reqs = status == StatusCode::TOO_MANY_REQUESTS;
1429 let rate_limit_503 = is_throttled_service_unavailable(status, headers);
1430 let vercel_backoff = is_vercel_rate_limit_challenge(status, headers);
1431
1432 too_many_reqs || rate_limit_503 || vercel_backoff
1433}
1434
1435fn is_throttled_service_unavailable(status: StatusCode, headers: &HeaderMap) -> bool {
1444 status == StatusCode::SERVICE_UNAVAILABLE && headers.contains_key(RETRY_AFTER)
1445}
1446
1447const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
1448const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
1449
1450fn is_vercel_rate_limit_challenge(status: StatusCode, headers: &HeaderMap) -> bool {
1452 let status = status == StatusCode::FORBIDDEN;
1453 let header = headers
1454 .get(VERCEL_CHALLENGE_HEADER)
1455 .is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
1456 let content_type = headers
1457 .get(CONTENT_TYPE)
1458 .and_then(|value| value.to_str().ok())
1459 .and_then(|value| value.parse::<Mime>().ok())
1460 .is_some_and(|mime_type| {
1461 mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
1462 });
1463
1464 status && header && content_type
1465}
1466
1467#[cfg(not(target_arch = "wasm32"))]
1468const MAX_ERR_SOURCE_ITERATIONS: usize = 6;
1469
1470#[cfg(not(target_arch = "wasm32"))]
1479pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
1480 if err.is_timeout() {
1481 return true;
1482 }
1483
1484 if !(err.is_connect() || err.is_request()) {
1485 return false;
1486 }
1487
1488 let mut inner = err.source();
1493 for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
1494 if let Some(e) = inner {
1495 if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
1496 match io_err.kind() {
1498 ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
1500 ErrorKind::ConnectionReset
1502 | ErrorKind::HostUnreachable
1503 | ErrorKind::ConnectionRefused => return true,
1504 ErrorKind::Other | ErrorKind::InvalidData => {
1506 inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
1509 }
1510 _ => return false,
1511 }
1512 } else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
1513 return true;
1515 } else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
1516 return resolve_err.is_nx_domain()
1520 || matches!(
1521 resolve_err,
1522 hickory_resolver::net::NetError::Dns(
1523 hickory_resolver::net::DnsError::ResponseCode(
1524 hickory_resolver::proto::op::ResponseCode::ServFail
1525 )
1526 )
1527 );
1528 } else if let Some(h2_err) = e.downcast_ref::<h2::Error>() {
1529 return h2_err.is_remote() && (h2_err.is_reset() || h2_err.is_go_away());
1538 } else {
1539 inner = e.source();
1540 }
1541 } else {
1542 break;
1543 }
1544 }
1545
1546 false
1547}
1548
1549#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1553#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1554pub trait ApiClient: ApiClientCore {
1555 fn create_get_request<P, K, V>(
1557 &self,
1558 path: P,
1559 params: Params<'_, K, V>,
1560 ) -> Result<RequestBuilder, HttpClientError>
1561 where
1562 P: RequestPath,
1563 K: AsRef<str>,
1564 V: AsRef<str>,
1565 {
1566 self.create_request(reqwest::Method::GET, path, params, None::<&()>)
1567 }
1568
1569 fn create_post_request<P, B, K, V>(
1571 &self,
1572 path: P,
1573 params: Params<'_, K, V>,
1574 json_body: &B,
1575 ) -> Result<RequestBuilder, HttpClientError>
1576 where
1577 P: RequestPath,
1578 B: Serialize + ?Sized,
1579 K: AsRef<str>,
1580 V: AsRef<str>,
1581 {
1582 self.create_request(reqwest::Method::POST, path, params, Some(json_body))
1583 }
1584
1585 fn create_delete_request<P, K, V>(
1587 &self,
1588 path: P,
1589 params: Params<'_, K, V>,
1590 ) -> Result<RequestBuilder, HttpClientError>
1591 where
1592 P: RequestPath,
1593 K: AsRef<str>,
1594 V: AsRef<str>,
1595 {
1596 self.create_request(reqwest::Method::DELETE, path, params, None::<&()>)
1597 }
1598
1599 fn create_patch_request<P, B, K, V>(
1601 &self,
1602 path: P,
1603 params: Params<'_, K, V>,
1604 json_body: &B,
1605 ) -> Result<RequestBuilder, HttpClientError>
1606 where
1607 P: RequestPath,
1608 B: Serialize + ?Sized,
1609 K: AsRef<str>,
1610 V: AsRef<str>,
1611 {
1612 self.create_request(reqwest::Method::PATCH, path, params, Some(json_body))
1613 }
1614
1615 #[instrument(level = "debug", skip_all, fields(path=?path))]
1617 async fn send_get_request<P, K, V>(
1618 &self,
1619 path: P,
1620 params: Params<'_, K, V>,
1621 ) -> Result<Response, HttpClientError>
1622 where
1623 P: RequestPath + Send + Sync,
1624 K: AsRef<str> + Sync,
1625 V: AsRef<str> + Sync,
1626 {
1627 self.send_request(reqwest::Method::GET, path, params, None::<&()>)
1628 .await
1629 }
1630
1631 async fn send_post_request<P, B, K, V>(
1633 &self,
1634 path: P,
1635 params: Params<'_, K, V>,
1636 json_body: &B,
1637 ) -> Result<Response, HttpClientError>
1638 where
1639 P: RequestPath + Send + Sync,
1640 B: Serialize + ?Sized + Sync,
1641 K: AsRef<str> + Sync,
1642 V: AsRef<str> + Sync,
1643 {
1644 self.send_request(reqwest::Method::POST, path, params, Some(json_body))
1645 .await
1646 }
1647
1648 async fn send_delete_request<P, K, V>(
1650 &self,
1651 path: P,
1652 params: Params<'_, K, V>,
1653 ) -> Result<Response, HttpClientError>
1654 where
1655 P: RequestPath + Send + Sync,
1656 K: AsRef<str> + Sync,
1657 V: AsRef<str> + Sync,
1658 {
1659 self.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1660 .await
1661 }
1662
1663 async fn send_patch_request<P, B, K, V>(
1665 &self,
1666 path: P,
1667 params: Params<'_, K, V>,
1668 json_body: &B,
1669 ) -> Result<Response, HttpClientError>
1670 where
1671 P: RequestPath + Send + Sync,
1672 B: Serialize + ?Sized + Sync,
1673 K: AsRef<str> + Sync,
1674 V: AsRef<str> + Sync,
1675 {
1676 self.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1677 .await
1678 }
1679
1680 #[instrument(level = "debug", skip_all, fields(path=?path))]
1684 async fn get_json<P, T, K, V>(
1686 &self,
1687 path: P,
1688 params: Params<'_, K, V>,
1689 ) -> Result<T, HttpClientError>
1690 where
1691 P: RequestPath + Send + Sync,
1692 for<'a> T: Deserialize<'a>,
1693 K: AsRef<str> + Sync,
1694 V: AsRef<str> + Sync,
1695 {
1696 self.get_response(path, params).await
1697 }
1698
1699 async fn parse_response<T>(
1701 &self,
1702 res: Response,
1703 allow_empty: bool,
1704 ) -> Result<T, HttpClientError>
1705 where
1706 T: DeserializeOwned,
1707 {
1708 let url = Url::from(res.url());
1709 parse_response(res, allow_empty).await.inspect_err(|e| {
1710 if matches!(
1711 e,
1714 HttpClientError::ResponseReadFailure {
1715 url: _,
1716 headers: _,
1717 status: _,
1718 source: _,
1719 }
1720 ) {
1721 self.maybe_rotate_hosts(Some(url.clone()));
1722 #[cfg(feature = "tunneling")]
1723 self.maybe_enable_fronting(url.host_str(), ("parse/read", url.as_str(), e));
1724 }
1725 })
1726 }
1727
1728 #[instrument(level = "debug", skip_all, fields(path=?path))]
1732 async fn get_response<P, T, K, V>(
1733 &self,
1734 path: P,
1735 params: Params<'_, K, V>,
1736 ) -> Result<T, HttpClientError>
1737 where
1738 P: RequestPath + Send + Sync,
1739 for<'a> T: Deserialize<'a>,
1740 K: AsRef<str> + Sync,
1741 V: AsRef<str> + Sync,
1742 {
1743 let res = self
1744 .send_request(reqwest::Method::GET, path, params, None::<&()>)
1745 .await?;
1746
1747 self.parse_response(res, false).await
1748 }
1749
1750 async fn post_json<P, B, T, K, V>(
1754 &self,
1755 path: P,
1756 params: Params<'_, K, V>,
1757 json_body: &B,
1758 ) -> Result<T, HttpClientError>
1759 where
1760 P: RequestPath + Send + Sync,
1761 B: Serialize + ?Sized + Sync,
1762 for<'a> T: Deserialize<'a>,
1763 K: AsRef<str> + Sync,
1764 V: AsRef<str> + Sync,
1765 {
1766 let res = self
1767 .send_request(reqwest::Method::POST, path, params, Some(json_body))
1768 .await?;
1769 self.parse_response(res, false).await
1770 }
1771
1772 async fn delete_json<P, T, K, V>(
1776 &self,
1777 path: P,
1778 params: Params<'_, K, V>,
1779 ) -> Result<T, HttpClientError>
1780 where
1781 P: RequestPath + Send + Sync,
1782 for<'a> T: Deserialize<'a>,
1783 K: AsRef<str> + Sync,
1784 V: AsRef<str> + Sync,
1785 {
1786 let res = self
1787 .send_request(reqwest::Method::DELETE, path, params, None::<&()>)
1788 .await?;
1789 self.parse_response(res, false).await
1790 }
1791
1792 async fn patch_json<P, B, T, K, V>(
1796 &self,
1797 path: P,
1798 params: Params<'_, K, V>,
1799 json_body: &B,
1800 ) -> Result<T, HttpClientError>
1801 where
1802 P: RequestPath + Send + Sync,
1803 B: Serialize + ?Sized + Sync,
1804 for<'a> T: Deserialize<'a>,
1805 K: AsRef<str> + Sync,
1806 V: AsRef<str> + Sync,
1807 {
1808 let res = self
1809 .send_request(reqwest::Method::PATCH, path, params, Some(json_body))
1810 .await?;
1811 self.parse_response(res, false).await
1812 }
1813
1814 async fn get_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1817 where
1818 for<'a> T: Deserialize<'a>,
1819 S: AsRef<str> + Sync + Send,
1820 {
1821 let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
1822 let res = self.send(req).await?;
1823 self.parse_response(res, false).await
1824 }
1825
1826 async fn post_json_data_to<B, T, S>(
1829 &self,
1830 endpoint: S,
1831 json_body: &B,
1832 ) -> Result<T, HttpClientError>
1833 where
1834 B: Serialize + ?Sized + Sync,
1835 for<'a> T: Deserialize<'a>,
1836 S: AsRef<str> + Sync + Send,
1837 {
1838 let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
1839 let res = self.send(req).await?;
1840 self.parse_response(res, false).await
1841 }
1842
1843 async fn delete_json_from<T, S>(&self, endpoint: S) -> Result<T, HttpClientError>
1846 where
1847 for<'a> T: Deserialize<'a>,
1848 S: AsRef<str> + Sync + Send,
1849 {
1850 let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
1851 let res = self.send(req).await?;
1852 self.parse_response(res, false).await
1853 }
1854
1855 async fn patch_json_data_at<B, T, S>(
1858 &self,
1859 endpoint: S,
1860 json_body: &B,
1861 ) -> Result<T, HttpClientError>
1862 where
1863 B: Serialize + ?Sized + Sync,
1864 for<'a> T: Deserialize<'a>,
1865 S: AsRef<str> + Sync + Send,
1866 {
1867 let req =
1868 self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
1869 let res = self.send(req).await?;
1870 self.parse_response(res, false).await
1871 }
1872}
1873
1874#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1875#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1876impl<C> ApiClient for C where C: ApiClientCore + Sync {}
1877
1878fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
1880 base: &Url,
1881 request_path: impl RequestPath,
1882 params: Params<'_, K, V>,
1883) -> Url {
1884 let mut url = base.clone();
1885 let mut path_segments = url
1886 .path_segments_mut()
1887 .expect("provided validator url does not have a base!");
1888
1889 path_segments.pop_if_empty();
1890
1891 for segment in request_path.to_sanitized_segments() {
1892 path_segments.push(segment);
1893 }
1894
1895 drop(path_segments);
1898
1899 if !params.is_empty() {
1900 url.query_pairs_mut().extend_pairs(params);
1901 }
1902
1903 url
1904}
1905
1906fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String {
1907 use encoding_rs::{Encoding, UTF_8};
1908
1909 let content_type = try_get_mime_type(headers);
1910
1911 let encoding_name = content_type
1912 .as_ref()
1913 .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
1914 .unwrap_or("utf-8");
1915
1916 let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
1917
1918 let (text, _, _) = encoding.decode(bytes);
1919 text.into_owned()
1920}
1921
1922#[instrument(level = "debug", skip_all)]
1924pub async fn parse_response<T>(res: Response, allow_empty: bool) -> Result<T, HttpClientError>
1925where
1926 T: DeserializeOwned,
1927{
1928 let status = res.status();
1929 let headers = res.headers().clone();
1930 let url = res.url().clone();
1931
1932 tracing::trace!("status: {status} (success: {})", status.is_success());
1933 tracing::trace!("headers: {headers:?}");
1934
1935 if !allow_empty && let Some(0) = res.content_length() {
1936 return Err(HttpClientError::EmptyResponse {
1937 url: Box::new(url),
1938 status,
1939 headers: Box::new(headers),
1940 });
1941 }
1942
1943 if res.status().is_success() {
1944 let full = res
1947 .bytes()
1948 .await
1949 .map_err(|source| HttpClientError::ResponseReadFailure {
1950 url: Box::new(url),
1951 headers: Box::new(headers.clone()),
1952 status,
1953 source: ReqwestErrorWrapper(source),
1954 })?;
1955 decode_raw_response(&headers, full)
1956 } else if res.status() == StatusCode::NOT_FOUND {
1957 Err(HttpClientError::NotFound { url: Box::new(url) })
1958 } else if is_http_rate_limit_err(&res) {
1959 Err(HttpClientError::EndpointFailure {
1960 url: Box::new(url),
1961 status,
1962 headers: Box::new(headers),
1963 error: String::from("received rate limit response"),
1964 })
1965 } else {
1966 let Ok(plaintext) = res.text().await else {
1967 return Err(HttpClientError::RequestFailure {
1968 url: Box::new(url),
1969 status,
1970 headers: Box::new(headers),
1971 });
1972 };
1973
1974 Err(HttpClientError::EndpointFailure {
1975 url: Box::new(url),
1976 status,
1977 headers: Box::new(headers),
1978 error: plaintext,
1979 })
1980 }
1981}
1982
1983fn decode_as_json<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
1984where
1985 T: DeserializeOwned,
1986{
1987 match serde_json::from_slice(&content) {
1988 Ok(data) => Ok(data),
1989 Err(err) => {
1990 let content = decode_as_text(&content, headers);
1991 Err(HttpClientError::ResponseDecodeFailure {
1992 message: err.to_string(),
1993 content,
1994 })
1995 }
1996 }
1997}
1998
1999fn decode_as_bincode<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
2000where
2001 T: DeserializeOwned,
2002{
2003 use bincode::Options;
2004
2005 let opts = nym_http_api_common::make_bincode_serializer();
2006 match opts.deserialize(&content) {
2007 Ok(data) => Ok(data),
2008 Err(err) => {
2009 let content = decode_as_text(&content, headers);
2010 Err(HttpClientError::ResponseDecodeFailure {
2011 message: err.to_string(),
2012 content,
2013 })
2014 }
2015 }
2016}
2017
2018fn decode_raw_response<T>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError>
2019where
2020 T: DeserializeOwned,
2021{
2022 let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON);
2024
2025 debug!("attempting to parse response as {mime}");
2026
2027 match (mime.type_(), mime.subtype().as_str()) {
2029 (mime::APPLICATION, "json") => decode_as_json(headers, content),
2030 (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content),
2031 (_, _) => {
2032 debug!("unrecognised mime type {mime}. falling back to json decoding...");
2033 decode_as_json(headers, content)
2034 }
2035 }
2036}
2037
2038fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
2039 headers
2040 .get(CONTENT_TYPE)
2041 .and_then(|value| value.to_str().ok())
2042 .and_then(|value| value.parse::<Mime>().ok())
2043}
2044
2045#[cfg(test)]
2046mod tests;