1use std::{borrow::Cow, collections::HashMap, str::FromStr, sync::Arc, time::Duration};
19
20use nautilus_core::collections::into_ustr_vec;
21use nautilus_cryptography::providers::install_cryptographic_provider;
22use reqwest::{
23 Method, Response, Url,
24 header::{HeaderMap, HeaderName, HeaderValue},
25};
26use ustr::Ustr;
27
28use super::{HttpClientError, HttpResponse, HttpStatus};
29use crate::ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota};
30
31const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 32;
33
34const DEFAULT_POOL_IDLE_TIMEOUT_SECS: u64 = 60;
36
37const DEFAULT_HTTP2_KEEP_ALIVE_SECS: u64 = 30;
39
40const DEFAULT_MAX_RESPONSE_BYTES: usize = 100 * 1024 * 1024;
46
47#[derive(Clone, Debug)]
57#[cfg_attr(
58 feature = "python",
59 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network", from_py_object)
60)]
61#[cfg_attr(
62 feature = "python",
63 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.network")
64)]
65pub struct HttpClient {
66 pub(crate) client: InnerHttpClient,
68 pub(crate) rate_limiter: Arc<RateLimiter<Ustr, MonotonicClock>>,
70}
71
72impl HttpClient {
73 pub fn new(
80 headers: HashMap<String, String>,
81 header_keys: Vec<String>,
82 keyed_quotas: Vec<(String, Quota)>,
83 default_quota: Option<Quota>,
84 timeout_secs: Option<u64>,
85 proxy_url: Option<String>,
86 ) -> Result<Self, HttpClientError> {
87 let keyed_quotas = keyed_quotas
88 .into_iter()
89 .map(|(key, quota)| (Ustr::from(&key), quota))
90 .collect();
91
92 let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas));
93
94 Self::new_with_rate_limiter(headers, header_keys, timeout_secs, proxy_url, rate_limiter)
95 }
96
97 pub fn new_with_rate_limiter(
110 headers: HashMap<String, String>,
111 header_keys: Vec<String>,
112 timeout_secs: Option<u64>,
113 proxy_url: Option<String>,
114 rate_limiter: Arc<RateLimiter<Ustr, MonotonicClock>>,
115 ) -> Result<Self, HttpClientError> {
116 install_cryptographic_provider();
117
118 let mut header_map = HeaderMap::new();
120
121 for (key, value) in headers {
122 let header_name = HeaderName::from_str(&key)
123 .map_err(|e| HttpClientError::Error(format!("Invalid header name '{key}': {e}")))?;
124 let header_value = HeaderValue::from_str(&value).map_err(|e| {
125 HttpClientError::Error(format!("Invalid header value '{value}': {e}"))
126 })?;
127 header_map.insert(header_name, header_value);
128 }
129
130 let mut client_builder = reqwest::Client::builder()
131 .default_headers(header_map)
132 .tcp_nodelay(true)
133 .pool_max_idle_per_host(DEFAULT_POOL_MAX_IDLE_PER_HOST)
134 .pool_idle_timeout(Duration::from_secs(DEFAULT_POOL_IDLE_TIMEOUT_SECS))
135 .http2_keep_alive_interval(Duration::from_secs(DEFAULT_HTTP2_KEEP_ALIVE_SECS))
136 .http2_keep_alive_while_idle(true)
137 .http2_adaptive_window(true);
138
139 if let Some(timeout_secs) = timeout_secs {
140 client_builder = client_builder.timeout(Duration::from_secs(timeout_secs));
141 }
142
143 if let Some(proxy_url) = proxy_url {
145 let proxy = reqwest::Proxy::all(&proxy_url)
146 .map_err(|e| HttpClientError::InvalidProxy(format!("{proxy_url}: {e}")))?;
147 client_builder = client_builder.proxy(proxy);
148 }
149
150 let client = client_builder
151 .build()
152 .map_err(|e| HttpClientError::ClientBuildError(e.to_string()))?;
153
154 let (valid_keys, header_names): (Vec<String>, Vec<HeaderName>) = header_keys
157 .into_iter()
158 .map(|k| {
159 HeaderName::from_str(&k)
160 .map(|name| (k.clone(), name))
161 .map_err(|e| HttpClientError::Error(format!("Invalid header key '{k}': {e}")))
162 })
163 .collect::<Result<Vec<_>, _>>()?
164 .into_iter()
165 .unzip();
166
167 let client = InnerHttpClient {
168 client,
169 header_keys: Arc::from(valid_keys),
170 header_names: Arc::from(header_names),
171 max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
172 };
173
174 Ok(Self {
175 client,
176 rate_limiter,
177 })
178 }
179
180 #[expect(clippy::too_many_arguments)]
190 pub async fn request(
191 &self,
192 method: Method,
193 url: String,
194 params: Option<&HashMap<String, Vec<String>>>,
195 headers: Option<HashMap<String, String>>,
196 body: Option<Vec<u8>>,
197 timeout_secs: Option<u64>,
198 keys: Option<Vec<String>>,
199 ) -> Result<HttpResponse, HttpClientError> {
200 let keys = keys.map(into_ustr_vec);
201
202 self.request_with_ustr_keys(method, url, params, headers, body, timeout_secs, keys)
203 .await
204 }
205
206 #[expect(clippy::too_many_arguments)]
216 pub async fn request_with_params<P: serde::Serialize>(
217 &self,
218 method: Method,
219 url: String,
220 params: Option<&P>,
221 headers: Option<HashMap<String, String>>,
222 body: Option<Vec<u8>>,
223 timeout_secs: Option<u64>,
224 keys: Option<Vec<String>>,
225 ) -> Result<HttpResponse, HttpClientError> {
226 let keys = keys.map(into_ustr_vec);
227 let rate_limiter = self.rate_limiter.clone();
228 rate_limiter.await_keys_ready(keys.as_deref()).await;
229
230 self.client
231 .send_request_with_query(method, url, params, headers, body, timeout_secs)
232 .await
233 }
234
235 #[expect(clippy::too_many_arguments)]
241 pub async fn request_with_ustr_keys(
242 &self,
243 method: Method,
244 url: String,
245 params: Option<&HashMap<String, Vec<String>>>,
246 headers: Option<HashMap<String, String>>,
247 body: Option<Vec<u8>>,
248 timeout_secs: Option<u64>,
249 keys: Option<Vec<Ustr>>,
250 ) -> Result<HttpResponse, HttpClientError> {
251 let rate_limiter = self.rate_limiter.clone();
252 rate_limiter.await_keys_ready(keys.as_deref()).await;
253
254 self.client
255 .send_request(method, url, params, headers, body, timeout_secs)
256 .await
257 }
258
259 pub async fn get(
265 &self,
266 url: String,
267 params: Option<&HashMap<String, Vec<String>>>,
268 headers: Option<HashMap<String, String>>,
269 timeout_secs: Option<u64>,
270 keys: Option<Vec<String>>,
271 ) -> Result<HttpResponse, HttpClientError> {
272 self.request(Method::GET, url, params, headers, None, timeout_secs, keys)
273 .await
274 }
275
276 pub async fn post(
282 &self,
283 url: String,
284 params: Option<&HashMap<String, Vec<String>>>,
285 headers: Option<HashMap<String, String>>,
286 body: Option<Vec<u8>>,
287 timeout_secs: Option<u64>,
288 keys: Option<Vec<String>>,
289 ) -> Result<HttpResponse, HttpClientError> {
290 self.request(Method::POST, url, params, headers, body, timeout_secs, keys)
291 .await
292 }
293
294 pub async fn patch(
300 &self,
301 url: String,
302 params: Option<&HashMap<String, Vec<String>>>,
303 headers: Option<HashMap<String, String>>,
304 body: Option<Vec<u8>>,
305 timeout_secs: Option<u64>,
306 keys: Option<Vec<String>>,
307 ) -> Result<HttpResponse, HttpClientError> {
308 self.request(
309 Method::PATCH,
310 url,
311 params,
312 headers,
313 body,
314 timeout_secs,
315 keys,
316 )
317 .await
318 }
319
320 pub async fn delete(
326 &self,
327 url: String,
328 params: Option<&HashMap<String, Vec<String>>>,
329 headers: Option<HashMap<String, String>>,
330 timeout_secs: Option<u64>,
331 keys: Option<Vec<String>>,
332 ) -> Result<HttpResponse, HttpClientError> {
333 self.request(
334 Method::DELETE,
335 url,
336 params,
337 headers,
338 None,
339 timeout_secs,
340 keys,
341 )
342 .await
343 }
344}
345
346#[derive(Clone, Debug)]
355pub struct InnerHttpClient {
356 pub(crate) client: reqwest::Client,
357 pub(crate) header_keys: Arc<[String]>,
358 pub(crate) header_names: Arc<[HeaderName]>,
359 pub(crate) max_response_bytes: usize,
361}
362
363impl InnerHttpClient {
364 pub async fn send_request(
370 &self,
371 method: Method,
372 url: String,
373 params: Option<&HashMap<String, Vec<String>>>,
374 headers: Option<HashMap<String, String>>,
375 body: Option<Vec<u8>>,
376 timeout_secs: Option<u64>,
377 ) -> Result<HttpResponse, HttpClientError> {
378 let full_url = encode_url_params(&url, params)?;
379 self.send_request_internal(
380 method,
381 full_url.as_ref(),
382 None::<&()>,
383 headers,
384 body,
385 timeout_secs,
386 )
387 .await
388 }
389
390 pub async fn send_request_with_query<Q: serde::Serialize>(
399 &self,
400 method: Method,
401 url: String,
402 query: Option<&Q>,
403 headers: Option<HashMap<String, String>>,
404 body: Option<Vec<u8>>,
405 timeout_secs: Option<u64>,
406 ) -> Result<HttpResponse, HttpClientError> {
407 self.send_request_internal(method, &url, query, headers, body, timeout_secs)
408 .await
409 }
410
411 async fn send_request_internal<Q: serde::Serialize>(
417 &self,
418 method: Method,
419 url: &str,
420 query: Option<&Q>,
421 headers: Option<HashMap<String, String>>,
422 body: Option<Vec<u8>>,
423 timeout_secs: Option<u64>,
424 ) -> Result<HttpResponse, HttpClientError> {
425 let reqwest_url =
426 Url::parse(url).map_err(|e| HttpClientError::from(format!("URL parse error: {e}")))?;
427
428 let mut request_builder = self.client.request(method, reqwest_url);
429
430 if let Some(headers) = headers {
431 let mut header_map = HeaderMap::with_capacity(headers.len());
432 for (header_key, header_value) in &headers {
433 let key = HeaderName::from_bytes(header_key.as_bytes())
434 .map_err(|e| HttpClientError::from(format!("Invalid header name: {e}")))?;
435
436 if let Some(old_value) = header_map.insert(
437 key.clone(),
438 header_value
439 .parse()
440 .map_err(|e| HttpClientError::from(format!("Invalid header value: {e}")))?,
441 ) {
442 log::trace!("Replaced header '{key}': old={old_value:?}, new={header_value}");
443 }
444 }
445 request_builder = request_builder.headers(header_map);
446 }
447
448 if let Some(q) = query {
449 request_builder = request_builder.query(q);
450 }
451
452 if let Some(timeout_secs) = timeout_secs {
453 request_builder = request_builder.timeout(Duration::new(timeout_secs, 0));
454 }
455
456 let request = match body {
457 Some(b) => request_builder
458 .body(b)
459 .build()
460 .map_err(HttpClientError::from)?,
461 None => request_builder.build().map_err(HttpClientError::from)?,
462 };
463
464 log::trace!("{} {}", request.method(), request.url());
465
466 let response = self
467 .client
468 .execute(request)
469 .await
470 .map_err(HttpClientError::from)?;
471
472 self.to_response(response).await
473 }
474
475 pub async fn to_response(&self, response: Response) -> Result<HttpResponse, HttpClientError> {
483 log::trace!("{response:?}");
484
485 let resp_headers = response.headers();
486 let mut headers =
487 HashMap::with_capacity(std::cmp::min(self.header_names.len(), resp_headers.len()));
488
489 for (name, key_str) in self.header_names.iter().zip(self.header_keys.iter()) {
490 if let Some(val) = resp_headers.get(name)
491 && let Ok(v) = val.to_str()
492 {
493 headers.insert(key_str.clone(), v.to_owned());
494 }
495 }
496
497 let status = HttpStatus::new(response.status());
498 let body = self.read_body_capped(response).await?;
499
500 Ok(HttpResponse {
501 status,
502 headers,
503 body,
504 })
505 }
506
507 async fn read_body_capped(
519 &self,
520 mut response: Response,
521 ) -> Result<bytes::Bytes, HttpClientError> {
522 let max = self.max_response_bytes;
523
524 if let Some(len) = response.content_length()
526 && len > max as u64
527 {
528 return Err(HttpClientError::Error(format!(
529 "HTTP response body of {len} bytes exceeds maximum of {max} bytes",
530 )));
531 }
532
533 let mut buf = bytes::BytesMut::new();
534 while let Some(chunk) = response.chunk().await.map_err(HttpClientError::from)? {
535 if buf.len() + chunk.len() > max {
536 return Err(HttpClientError::Error(format!(
537 "HTTP response body exceeds maximum of {max} bytes",
538 )));
539 }
540 buf.extend_from_slice(&chunk);
541 }
542
543 Ok(buf.freeze())
544 }
545}
546
547impl Default for InnerHttpClient {
548 fn default() -> Self {
552 install_cryptographic_provider();
553 let client = reqwest::Client::new();
554 Self {
555 client,
556 header_keys: Arc::default(),
557 header_names: Arc::default(),
558 max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
559 }
560 }
561}
562
563fn encode_url_params<'a>(
569 url: &'a str,
570 params: Option<&HashMap<String, Vec<String>>>,
571) -> Result<Cow<'a, str>, HttpClientError> {
572 let Some(params) = params else {
573 return Ok(Cow::Borrowed(url));
574 };
575
576 let pairs: Vec<(&str, &str)> = params
577 .iter()
578 .flat_map(|(key, values)| {
579 values
580 .iter()
581 .map(move |value| (key.as_str(), value.as_str()))
582 })
583 .collect();
584
585 if pairs.is_empty() {
586 return Ok(Cow::Borrowed(url));
587 }
588
589 let query_string = serde_urlencoded::to_string(pairs)
590 .map_err(|e| HttpClientError::Error(format!("Failed to encode params: {e}")))?;
591
592 let separator = if url.contains('?') { '&' } else { '?' };
593 Ok(Cow::Owned(format!("{url}{separator}{query_string}")))
594}
595
596#[cfg(test)]
597#[cfg(target_os = "linux")] mod tests {
599 use std::net::SocketAddr;
600
601 use axum::{
602 Router,
603 routing::{delete, get, patch, post},
604 serve,
605 };
606 use http::status::StatusCode;
607 use rstest::rstest;
608
609 use super::*;
610
611 fn create_router() -> Router {
612 Router::new()
613 .route("/get", get(|| async { "hello-world!" }))
614 .route("/post", post(|| async { StatusCode::OK }))
615 .route("/patch", patch(|| async { StatusCode::OK }))
616 .route("/delete", delete(|| async { StatusCode::OK }))
617 .route("/notfound", get(|| async { StatusCode::NOT_FOUND }))
618 .route(
619 "/slow",
620 get(|| async {
621 tokio::time::sleep(Duration::from_secs(2)).await;
622 "Eventually responded"
623 }),
624 )
625 .route(
626 "/large",
627 get(|| async { "x".repeat(1024 * 1024) }),
629 )
630 }
631
632 async fn start_test_server() -> Result<SocketAddr, Box<dyn std::error::Error + Send + Sync>> {
633 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
634 let addr = listener.local_addr().unwrap();
635
636 tokio::spawn(async move {
637 serve(listener, create_router()).await.unwrap();
638 });
639
640 Ok(addr)
641 }
642
643 #[tokio::test]
644 async fn test_get() {
645 let addr = start_test_server().await.unwrap();
646 let url = format!("http://{addr}");
647
648 let client = InnerHttpClient::default();
649 let response = client
650 .send_request(
651 reqwest::Method::GET,
652 format!("{url}/get"),
653 None,
654 None,
655 None,
656 None,
657 )
658 .await
659 .unwrap();
660
661 assert!(response.status.is_success());
662 assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
663 }
664
665 #[tokio::test]
666 async fn test_response_body_within_cap_is_returned() {
667 let addr = start_test_server().await.unwrap();
668 let url = format!("http://{addr}");
669
670 let client = InnerHttpClient {
672 max_response_bytes: 4 * 1024 * 1024,
673 ..Default::default()
674 };
675
676 let response = client
677 .send_request(
678 reqwest::Method::GET,
679 format!("{url}/large"),
680 None,
681 None,
682 None,
683 None,
684 )
685 .await
686 .unwrap();
687
688 assert!(response.status.is_success());
689 assert_eq!(response.body.len(), 1024 * 1024);
690 }
691
692 #[tokio::test]
693 async fn test_response_body_exceeding_cap_is_rejected() {
694 let addr = start_test_server().await.unwrap();
695 let url = format!("http://{addr}");
696
697 let client = InnerHttpClient {
699 max_response_bytes: 16 * 1024,
700 ..Default::default()
701 };
702
703 let result = client
704 .send_request(
705 reqwest::Method::GET,
706 format!("{url}/large"),
707 None,
708 None,
709 None,
710 None,
711 )
712 .await;
713
714 let err = result.expect_err("oversized response body should be rejected");
715 assert!(
716 err.to_string().contains("exceeds maximum"),
717 "unexpected error: {err}",
718 );
719 }
720
721 #[tokio::test]
722 async fn test_post() {
723 let addr = start_test_server().await.unwrap();
724 let url = format!("http://{addr}");
725
726 let client = InnerHttpClient::default();
727 let response = client
728 .send_request(
729 reqwest::Method::POST,
730 format!("{url}/post"),
731 None,
732 None,
733 None,
734 None,
735 )
736 .await
737 .unwrap();
738
739 assert!(response.status.is_success());
740 }
741
742 #[tokio::test]
743 async fn test_post_with_body() {
744 let addr = start_test_server().await.unwrap();
745 let url = format!("http://{addr}");
746
747 let client = InnerHttpClient::default();
748
749 let mut body = HashMap::new();
750 body.insert(
751 "key1".to_string(),
752 serde_json::Value::String("value1".to_string()),
753 );
754 body.insert(
755 "key2".to_string(),
756 serde_json::Value::String("value2".to_string()),
757 );
758
759 let body_string = serde_json::to_string(&body).unwrap();
760 let body_bytes = body_string.into_bytes();
761
762 let response = client
763 .send_request(
764 reqwest::Method::POST,
765 format!("{url}/post"),
766 None,
767 None,
768 Some(body_bytes),
769 None,
770 )
771 .await
772 .unwrap();
773
774 assert!(response.status.is_success());
775 }
776
777 #[tokio::test]
778 async fn test_patch() {
779 let addr = start_test_server().await.unwrap();
780 let url = format!("http://{addr}");
781
782 let client = InnerHttpClient::default();
783 let response = client
784 .send_request(
785 reqwest::Method::PATCH,
786 format!("{url}/patch"),
787 None,
788 None,
789 None,
790 None,
791 )
792 .await
793 .unwrap();
794
795 assert!(response.status.is_success());
796 }
797
798 #[tokio::test]
799 async fn test_delete() {
800 let addr = start_test_server().await.unwrap();
801 let url = format!("http://{addr}");
802
803 let client = InnerHttpClient::default();
804 let response = client
805 .send_request(
806 reqwest::Method::DELETE,
807 format!("{url}/delete"),
808 None,
809 None,
810 None,
811 None,
812 )
813 .await
814 .unwrap();
815
816 assert!(response.status.is_success());
817 }
818
819 #[tokio::test]
820 async fn test_not_found() {
821 let addr = start_test_server().await.unwrap();
822 let url = format!("http://{addr}/notfound");
823 let client = InnerHttpClient::default();
824
825 let response = client
826 .send_request(reqwest::Method::GET, url, None, None, None, None)
827 .await
828 .unwrap();
829
830 assert!(response.status.is_client_error());
831 assert_eq!(response.status.as_u16(), 404);
832 }
833
834 #[tokio::test]
835 async fn test_timeout() {
836 let addr = start_test_server().await.unwrap();
837 let url = format!("http://{addr}/slow");
838 let client = InnerHttpClient::default();
839
840 let result = client
842 .send_request(reqwest::Method::GET, url, None, None, None, Some(1))
843 .await;
844
845 match result {
846 Err(HttpClientError::TimeoutError(msg)) => {
847 println!("Got expected timeout error: {msg}");
848 }
849 Err(e) => panic!("Expected a timeout error, was: {e:?}"),
850 Ok(resp) => panic!("Expected a timeout error, but was a successful response: {resp:?}"),
851 }
852 }
853
854 #[rstest]
855 fn test_http_client_without_proxy() {
856 let result = HttpClient::new(
858 HashMap::new(),
859 vec![],
860 vec![],
861 None,
862 None,
863 None, );
865
866 assert!(result.is_ok());
867 }
868
869 #[rstest]
870 fn test_http_client_with_valid_proxy() {
871 let result = HttpClient::new(
873 HashMap::new(),
874 vec![],
875 vec![],
876 None,
877 None,
878 Some("http://proxy.example.com:8080".to_string()),
879 );
880
881 assert!(result.is_ok());
882 }
883
884 #[rstest]
885 fn test_http_client_with_socks5_proxy() {
886 let result = HttpClient::new(
888 HashMap::new(),
889 vec![],
890 vec![],
891 None,
892 None,
893 Some("socks5://127.0.0.1:1080".to_string()),
894 );
895
896 assert!(result.is_ok());
897 }
898
899 #[rstest]
900 fn test_http_client_with_malformed_proxy() {
901 let result = HttpClient::new(
905 HashMap::new(),
906 vec![],
907 vec![],
908 None,
909 None,
910 Some("://invalid".to_string()),
911 );
912
913 assert!(result.is_err());
914 assert!(matches!(result, Err(HttpClientError::InvalidProxy(_))));
915 }
916
917 #[rstest]
918 fn test_http_client_with_empty_proxy_string() {
919 let result = HttpClient::new(
921 HashMap::new(),
922 vec![],
923 vec![],
924 None,
925 None,
926 Some(String::new()),
927 );
928
929 assert!(result.is_err());
930 assert!(matches!(result, Err(HttpClientError::InvalidProxy(_))));
931 }
932
933 #[tokio::test]
934 async fn test_http_client_get() {
935 let addr = start_test_server().await.unwrap();
936 let url = format!("http://{addr}/get");
937
938 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
939 let response = client.get(url, None, None, None, None).await.unwrap();
940
941 assert!(response.status.is_success());
942 assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
943 }
944
945 #[tokio::test]
946 async fn test_http_client_post() {
947 let addr = start_test_server().await.unwrap();
948 let url = format!("http://{addr}/post");
949
950 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
951 let response = client
952 .post(url, None, None, None, None, None)
953 .await
954 .unwrap();
955
956 assert!(response.status.is_success());
957 }
958
959 #[tokio::test]
960 async fn test_http_client_patch() {
961 let addr = start_test_server().await.unwrap();
962 let url = format!("http://{addr}/patch");
963
964 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
965 let response = client
966 .patch(url, None, None, None, None, None)
967 .await
968 .unwrap();
969
970 assert!(response.status.is_success());
971 }
972
973 #[tokio::test]
974 async fn test_http_client_delete() {
975 let addr = start_test_server().await.unwrap();
976 let url = format!("http://{addr}/delete");
977
978 let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
979 let response = client.delete(url, None, None, None, None).await.unwrap();
980
981 assert!(response.status.is_success());
982 }
983}