Skip to main content

nautilus_network/http/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! HTTP client implementation with rate limiting and timeout support.
17
18use std::{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
31/// An HTTP client that supports rate limiting and timeouts.
32///
33/// Built on `reqwest` for async I/O. Allows per-endpoint and default quotas
34/// through a rate limiter.
35///
36/// This struct is designed to handle HTTP requests efficiently, providing
37/// support for rate limiting, timeouts, and custom headers. The client is
38/// built on top of `reqwest` and can be used for both synchronous and
39/// asynchronous HTTP requests.
40#[derive(Clone, Debug)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network", from_py_object)
44)]
45pub struct HttpClient {
46    /// The underlying HTTP client used to make requests.
47    pub(crate) client: InnerHttpClient,
48    /// The rate limiter to control the request rate.
49    pub(crate) rate_limiter: Arc<RateLimiter<Ustr, MonotonicClock>>,
50}
51
52impl HttpClient {
53    /// Creates a new [`HttpClient`] instance.
54    ///
55    /// # Errors
56    ///
57    /// - Returns `InvalidProxy` if the proxy URL is malformed.
58    /// - Returns `ClientBuildError` if building the underlying `reqwest::Client` fails.
59    pub fn new(
60        headers: HashMap<String, String>,
61        header_keys: Vec<String>,
62        keyed_quotas: Vec<(String, Quota)>,
63        default_quota: Option<Quota>,
64        timeout_secs: Option<u64>,
65        proxy_url: Option<String>,
66    ) -> Result<Self, HttpClientError> {
67        install_cryptographic_provider();
68
69        // Build default headers
70        let mut header_map = HeaderMap::new();
71        for (key, value) in headers {
72            let header_name = HeaderName::from_str(&key)
73                .map_err(|e| HttpClientError::Error(format!("Invalid header name '{key}': {e}")))?;
74            let header_value = HeaderValue::from_str(&value).map_err(|e| {
75                HttpClientError::Error(format!("Invalid header value '{value}': {e}"))
76            })?;
77            header_map.insert(header_name, header_value);
78        }
79
80        let mut client_builder = reqwest::Client::builder().default_headers(header_map);
81        client_builder = client_builder.tcp_nodelay(true);
82
83        if let Some(timeout_secs) = timeout_secs {
84            client_builder = client_builder.timeout(Duration::from_secs(timeout_secs));
85        }
86
87        // Configure proxy if provided
88        if let Some(proxy_url) = proxy_url {
89            let proxy = reqwest::Proxy::all(&proxy_url)
90                .map_err(|e| HttpClientError::InvalidProxy(format!("{proxy_url}: {e}")))?;
91            client_builder = client_builder.proxy(proxy);
92        }
93
94        let client = client_builder
95            .build()
96            .map_err(|e| HttpClientError::ClientBuildError(e.to_string()))?;
97
98        let client = InnerHttpClient {
99            client,
100            header_keys: Arc::new(header_keys),
101        };
102
103        let keyed_quotas = keyed_quotas
104            .into_iter()
105            .map(|(key, quota)| (Ustr::from(&key), quota))
106            .collect();
107
108        let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas));
109
110        Ok(Self {
111            client,
112            rate_limiter,
113        })
114    }
115
116    /// Sends an HTTP request.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if unable to send request or times out.
121    ///
122    /// # Examples
123    ///
124    /// If requesting `/foo/bar`, pass rate-limit keys `["foo/bar", "foo"]`.
125    #[allow(clippy::too_many_arguments)]
126    pub async fn request(
127        &self,
128        method: Method,
129        url: String,
130        params: Option<&HashMap<String, Vec<String>>>,
131        headers: Option<HashMap<String, String>>,
132        body: Option<Vec<u8>>,
133        timeout_secs: Option<u64>,
134        keys: Option<Vec<String>>,
135    ) -> Result<HttpResponse, HttpClientError> {
136        let keys = keys.map(into_ustr_vec);
137
138        self.request_with_ustr_keys(method, url, params, headers, body, timeout_secs, keys)
139            .await
140    }
141
142    /// Sends an HTTP request with serializable query parameters.
143    ///
144    /// This method accepts any type implementing `Serialize` for query parameters,
145    /// which will be automatically encoded into the URL query string using reqwest's
146    /// `.query()` method, avoiding unnecessary HashMap allocations.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if unable to send request or times out.
151    #[allow(clippy::too_many_arguments)]
152    pub async fn request_with_params<P: serde::Serialize>(
153        &self,
154        method: Method,
155        url: String,
156        params: Option<&P>,
157        headers: Option<HashMap<String, String>>,
158        body: Option<Vec<u8>>,
159        timeout_secs: Option<u64>,
160        keys: Option<Vec<String>>,
161    ) -> Result<HttpResponse, HttpClientError> {
162        let keys = keys.map(into_ustr_vec);
163        let rate_limiter = self.rate_limiter.clone();
164        rate_limiter.await_keys_ready(keys.as_deref()).await;
165
166        self.client
167            .send_request_with_query(method, url, params, headers, body, timeout_secs)
168            .await
169    }
170
171    /// Sends an HTTP request using pre-interned rate limiter keys.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if unable to send the request or the request times out.
176    #[allow(clippy::too_many_arguments)]
177    pub async fn request_with_ustr_keys(
178        &self,
179        method: Method,
180        url: String,
181        params: Option<&HashMap<String, Vec<String>>>,
182        headers: Option<HashMap<String, String>>,
183        body: Option<Vec<u8>>,
184        timeout_secs: Option<u64>,
185        keys: Option<Vec<Ustr>>,
186    ) -> Result<HttpResponse, HttpClientError> {
187        let rate_limiter = self.rate_limiter.clone();
188        rate_limiter.await_keys_ready(keys.as_deref()).await;
189
190        self.client
191            .send_request(method, url, params, headers, body, timeout_secs)
192            .await
193    }
194
195    /// Sends an HTTP GET request.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if unable to send request or times out.
200    pub async fn get(
201        &self,
202        url: String,
203        params: Option<&HashMap<String, Vec<String>>>,
204        headers: Option<HashMap<String, String>>,
205        timeout_secs: Option<u64>,
206        keys: Option<Vec<String>>,
207    ) -> Result<HttpResponse, HttpClientError> {
208        self.request(Method::GET, url, params, headers, None, timeout_secs, keys)
209            .await
210    }
211
212    /// Sends an HTTP POST request.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if unable to send request or times out.
217    pub async fn post(
218        &self,
219        url: String,
220        params: Option<&HashMap<String, Vec<String>>>,
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        self.request(Method::POST, url, params, headers, body, timeout_secs, keys)
227            .await
228    }
229
230    /// Sends an HTTP PATCH request.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if unable to send request or times out.
235    pub async fn patch(
236        &self,
237        url: String,
238        params: Option<&HashMap<String, Vec<String>>>,
239        headers: Option<HashMap<String, String>>,
240        body: Option<Vec<u8>>,
241        timeout_secs: Option<u64>,
242        keys: Option<Vec<String>>,
243    ) -> Result<HttpResponse, HttpClientError> {
244        self.request(
245            Method::PATCH,
246            url,
247            params,
248            headers,
249            body,
250            timeout_secs,
251            keys,
252        )
253        .await
254    }
255
256    /// Sends an HTTP DELETE request.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error if unable to send request or times out.
261    pub async fn delete(
262        &self,
263        url: String,
264        params: Option<&HashMap<String, Vec<String>>>,
265        headers: Option<HashMap<String, String>>,
266        timeout_secs: Option<u64>,
267        keys: Option<Vec<String>>,
268    ) -> Result<HttpResponse, HttpClientError> {
269        self.request(
270            Method::DELETE,
271            url,
272            params,
273            headers,
274            None,
275            timeout_secs,
276            keys,
277        )
278        .await
279    }
280}
281
282/// Internal implementation backing [`HttpClient`].
283///
284/// The client is backed by a [`reqwest::Client`] which keeps connections alive and
285/// can be cloned cheaply. The client also has a list of header fields to
286/// extract from the response.
287///
288/// The client returns an [`HttpResponse`]. The client filters only the key value
289/// for the give `header_keys`.
290#[derive(Clone, Debug)]
291pub struct InnerHttpClient {
292    pub(crate) client: reqwest::Client,
293    pub(crate) header_keys: Arc<Vec<String>>,
294}
295
296impl InnerHttpClient {
297    /// Sends an HTTP request and returns an [`HttpResponse`].
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if unable to send request or times out.
302    pub async fn send_request(
303        &self,
304        method: Method,
305        url: String,
306        params: Option<&HashMap<String, Vec<String>>>,
307        headers: Option<HashMap<String, String>>,
308        body: Option<Vec<u8>>,
309        timeout_secs: Option<u64>,
310    ) -> Result<HttpResponse, HttpClientError> {
311        let full_url = encode_url_params(&url, params)?;
312        self.send_request_internal(method, full_url, None::<&()>, headers, body, timeout_secs)
313            .await
314    }
315
316    /// Sends an HTTP request with query parameters using reqwest's `.query()` method.
317    ///
318    /// This method accepts any type implementing `Serialize` for query parameters,
319    /// avoiding HashMap conversion overhead.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error if unable to send request or times out.
324    pub async fn send_request_with_query<Q: serde::Serialize>(
325        &self,
326        method: Method,
327        url: String,
328        query: Option<&Q>,
329        headers: Option<HashMap<String, String>>,
330        body: Option<Vec<u8>>,
331        timeout_secs: Option<u64>,
332    ) -> Result<HttpResponse, HttpClientError> {
333        self.send_request_internal(method, url, query, headers, body, timeout_secs)
334            .await
335    }
336
337    /// Internal implementation for sending HTTP requests.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if unable to send request or times out.
342    async fn send_request_internal<Q: serde::Serialize>(
343        &self,
344        method: Method,
345        url: String,
346        query: Option<&Q>,
347        headers: Option<HashMap<String, String>>,
348        body: Option<Vec<u8>>,
349        timeout_secs: Option<u64>,
350    ) -> Result<HttpResponse, HttpClientError> {
351        let headers = headers.unwrap_or_default();
352        let reqwest_url = Url::parse(url.as_str())
353            .map_err(|e| HttpClientError::from(format!("URL parse error: {e}")))?;
354
355        let mut header_map = HeaderMap::new();
356        for (header_key, header_value) in &headers {
357            let key = HeaderName::from_bytes(header_key.as_bytes())
358                .map_err(|e| HttpClientError::from(format!("Invalid header name: {e}")))?;
359            if let Some(old_value) = header_map.insert(
360                key.clone(),
361                header_value
362                    .parse()
363                    .map_err(|e| HttpClientError::from(format!("Invalid header value: {e}")))?,
364            ) {
365                log::trace!("Replaced header '{key}': old={old_value:?}, new={header_value}");
366            }
367        }
368
369        let mut request_builder = self.client.request(method, reqwest_url).headers(header_map);
370
371        if let Some(q) = query {
372            request_builder = request_builder.query(q);
373        }
374
375        if let Some(timeout_secs) = timeout_secs {
376            request_builder = request_builder.timeout(Duration::new(timeout_secs, 0));
377        }
378
379        let request = match body {
380            Some(b) => request_builder
381                .body(b)
382                .build()
383                .map_err(HttpClientError::from)?,
384            None => request_builder.build().map_err(HttpClientError::from)?,
385        };
386
387        log::trace!("{} {}", request.method(), request.url());
388
389        let response = self
390            .client
391            .execute(request)
392            .await
393            .map_err(HttpClientError::from)?;
394
395        self.to_response(response).await
396    }
397
398    /// Converts a `reqwest::Response` into an `HttpResponse`.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if unable to send request or times out.
403    pub async fn to_response(&self, response: Response) -> Result<HttpResponse, HttpClientError> {
404        log::trace!("{response:?}");
405
406        let headers: HashMap<String, String> = self
407            .header_keys
408            .iter()
409            .filter_map(|key| response.headers().get(key).map(|val| (key, val)))
410            .filter_map(|(key, val)| val.to_str().map(|v| (key, v)).ok())
411            .map(|(k, v)| (k.clone(), v.to_owned()))
412            .collect();
413        let status = HttpStatus::new(response.status());
414        let body = response.bytes().await.map_err(HttpClientError::from)?;
415
416        Ok(HttpResponse {
417            status,
418            headers,
419            body,
420        })
421    }
422}
423
424impl Default for InnerHttpClient {
425    /// Creates a new default [`InnerHttpClient`] instance.
426    ///
427    /// The default client is initialized with an empty list of header keys and a new `reqwest::Client`.
428    fn default() -> Self {
429        install_cryptographic_provider();
430        let client = reqwest::Client::new();
431        Self {
432            client,
433            header_keys: Default::default(),
434        }
435    }
436}
437
438/// Helper function to encode URL parameters.
439///
440/// Takes a base URL and optional query parameters, returning the full URL with encoded query string.
441/// Parameters can have multiple values per key (for doseq=True behavior).
442/// Preserves existing query strings in the URL by appending with '&' instead of '?'.
443fn encode_url_params(
444    url: &str,
445    params: Option<&HashMap<String, Vec<String>>>,
446) -> Result<String, HttpClientError> {
447    let Some(params) = params else {
448        return Ok(url.to_string());
449    };
450
451    // Flatten HashMap<String, Vec<String>> into Vec<(String, String)> for serde_urlencoded
452    let pairs: Vec<(String, String)> = params
453        .iter()
454        .flat_map(|(key, values)| values.iter().map(move |value| (key.clone(), value.clone())))
455        .collect();
456
457    if pairs.is_empty() {
458        return Ok(url.to_string());
459    }
460
461    let query_string = serde_urlencoded::to_string(pairs)
462        .map_err(|e| HttpClientError::Error(format!("Failed to encode params: {e}")))?;
463
464    // Check if URL already has a query string
465    let separator = if url.contains('?') { '&' } else { '?' };
466    Ok(format!("{url}{separator}{query_string}"))
467}
468
469#[cfg(test)]
470#[cfg(target_os = "linux")] // Only run network tests on Linux (CI stability)
471mod tests {
472    use std::net::SocketAddr;
473
474    use axum::{
475        Router,
476        routing::{delete, get, patch, post},
477        serve,
478    };
479    use http::status::StatusCode;
480    use rstest::rstest;
481
482    use super::*;
483
484    fn create_router() -> Router {
485        Router::new()
486            .route("/get", get(|| async { "hello-world!" }))
487            .route("/post", post(|| async { StatusCode::OK }))
488            .route("/patch", patch(|| async { StatusCode::OK }))
489            .route("/delete", delete(|| async { StatusCode::OK }))
490            .route("/notfound", get(|| async { StatusCode::NOT_FOUND }))
491            .route(
492                "/slow",
493                get(|| async {
494                    tokio::time::sleep(Duration::from_secs(2)).await;
495                    "Eventually responded"
496                }),
497            )
498    }
499
500    async fn start_test_server() -> Result<SocketAddr, Box<dyn std::error::Error + Send + Sync>> {
501        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
502        let addr = listener.local_addr().unwrap();
503
504        tokio::spawn(async move {
505            serve(listener, create_router()).await.unwrap();
506        });
507
508        Ok(addr)
509    }
510
511    #[tokio::test]
512    async fn test_get() {
513        let addr = start_test_server().await.unwrap();
514        let url = format!("http://{addr}");
515
516        let client = InnerHttpClient::default();
517        let response = client
518            .send_request(
519                reqwest::Method::GET,
520                format!("{url}/get"),
521                None,
522                None,
523                None,
524                None,
525            )
526            .await
527            .unwrap();
528
529        assert!(response.status.is_success());
530        assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
531    }
532
533    #[tokio::test]
534    async fn test_post() {
535        let addr = start_test_server().await.unwrap();
536        let url = format!("http://{addr}");
537
538        let client = InnerHttpClient::default();
539        let response = client
540            .send_request(
541                reqwest::Method::POST,
542                format!("{url}/post"),
543                None,
544                None,
545                None,
546                None,
547            )
548            .await
549            .unwrap();
550
551        assert!(response.status.is_success());
552    }
553
554    #[tokio::test]
555    async fn test_post_with_body() {
556        let addr = start_test_server().await.unwrap();
557        let url = format!("http://{addr}");
558
559        let client = InnerHttpClient::default();
560
561        let mut body = HashMap::new();
562        body.insert(
563            "key1".to_string(),
564            serde_json::Value::String("value1".to_string()),
565        );
566        body.insert(
567            "key2".to_string(),
568            serde_json::Value::String("value2".to_string()),
569        );
570
571        let body_string = serde_json::to_string(&body).unwrap();
572        let body_bytes = body_string.into_bytes();
573
574        let response = client
575            .send_request(
576                reqwest::Method::POST,
577                format!("{url}/post"),
578                None,
579                None,
580                Some(body_bytes),
581                None,
582            )
583            .await
584            .unwrap();
585
586        assert!(response.status.is_success());
587    }
588
589    #[tokio::test]
590    async fn test_patch() {
591        let addr = start_test_server().await.unwrap();
592        let url = format!("http://{addr}");
593
594        let client = InnerHttpClient::default();
595        let response = client
596            .send_request(
597                reqwest::Method::PATCH,
598                format!("{url}/patch"),
599                None,
600                None,
601                None,
602                None,
603            )
604            .await
605            .unwrap();
606
607        assert!(response.status.is_success());
608    }
609
610    #[tokio::test]
611    async fn test_delete() {
612        let addr = start_test_server().await.unwrap();
613        let url = format!("http://{addr}");
614
615        let client = InnerHttpClient::default();
616        let response = client
617            .send_request(
618                reqwest::Method::DELETE,
619                format!("{url}/delete"),
620                None,
621                None,
622                None,
623                None,
624            )
625            .await
626            .unwrap();
627
628        assert!(response.status.is_success());
629    }
630
631    #[tokio::test]
632    async fn test_not_found() {
633        let addr = start_test_server().await.unwrap();
634        let url = format!("http://{addr}/notfound");
635        let client = InnerHttpClient::default();
636
637        let response = client
638            .send_request(reqwest::Method::GET, url, None, None, None, None)
639            .await
640            .unwrap();
641
642        assert!(response.status.is_client_error());
643        assert_eq!(response.status.as_u16(), 404);
644    }
645
646    #[tokio::test]
647    async fn test_timeout() {
648        let addr = start_test_server().await.unwrap();
649        let url = format!("http://{addr}/slow");
650        let client = InnerHttpClient::default();
651
652        // We'll set a 1-second timeout for a route that sleeps 2 seconds
653        let result = client
654            .send_request(reqwest::Method::GET, url, None, None, None, Some(1))
655            .await;
656
657        match result {
658            Err(HttpClientError::TimeoutError(msg)) => {
659                println!("Got expected timeout error: {msg}");
660            }
661            Err(e) => panic!("Expected a timeout error, was: {e:?}"),
662            Ok(resp) => panic!("Expected a timeout error, but was a successful response: {resp:?}"),
663        }
664    }
665
666    #[rstest]
667    fn test_http_client_without_proxy() {
668        // Create client with no proxy
669        let result = HttpClient::new(
670            HashMap::new(),
671            vec![],
672            vec![],
673            None,
674            None,
675            None, // No proxy
676        );
677
678        assert!(result.is_ok());
679    }
680
681    #[rstest]
682    fn test_http_client_with_valid_proxy() {
683        // Create client with a valid proxy URL
684        let result = HttpClient::new(
685            HashMap::new(),
686            vec![],
687            vec![],
688            None,
689            None,
690            Some("http://proxy.example.com:8080".to_string()),
691        );
692
693        assert!(result.is_ok());
694    }
695
696    #[rstest]
697    fn test_http_client_with_socks5_proxy() {
698        // Create client with a SOCKS5 proxy URL
699        let result = HttpClient::new(
700            HashMap::new(),
701            vec![],
702            vec![],
703            None,
704            None,
705            Some("socks5://127.0.0.1:1080".to_string()),
706        );
707
708        assert!(result.is_ok());
709    }
710
711    #[rstest]
712    fn test_http_client_with_malformed_proxy() {
713        // Note: reqwest::Proxy::all() is lenient and accepts most strings.
714        // It only fails on obviously malformed URLs like "://invalid" or "http://".
715        // More subtle issues (like "not-a-valid-url") are caught when connecting.
716        let result = HttpClient::new(
717            HashMap::new(),
718            vec![],
719            vec![],
720            None,
721            None,
722            Some("://invalid".to_string()),
723        );
724
725        assert!(result.is_err());
726        assert!(matches!(result, Err(HttpClientError::InvalidProxy(_))));
727    }
728
729    #[rstest]
730    fn test_http_client_with_empty_proxy_string() {
731        // Create client with an empty proxy URL string
732        let result = HttpClient::new(
733            HashMap::new(),
734            vec![],
735            vec![],
736            None,
737            None,
738            Some(String::new()),
739        );
740
741        assert!(result.is_err());
742        assert!(matches!(result, Err(HttpClientError::InvalidProxy(_))));
743    }
744
745    #[tokio::test]
746    async fn test_http_client_get() {
747        let addr = start_test_server().await.unwrap();
748        let url = format!("http://{addr}/get");
749
750        let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
751        let response = client.get(url, None, None, None, None).await.unwrap();
752
753        assert!(response.status.is_success());
754        assert_eq!(String::from_utf8_lossy(&response.body), "hello-world!");
755    }
756
757    #[tokio::test]
758    async fn test_http_client_post() {
759        let addr = start_test_server().await.unwrap();
760        let url = format!("http://{addr}/post");
761
762        let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
763        let response = client
764            .post(url, None, None, None, None, None)
765            .await
766            .unwrap();
767
768        assert!(response.status.is_success());
769    }
770
771    #[tokio::test]
772    async fn test_http_client_patch() {
773        let addr = start_test_server().await.unwrap();
774        let url = format!("http://{addr}/patch");
775
776        let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
777        let response = client
778            .patch(url, None, None, None, None, None)
779            .await
780            .unwrap();
781
782        assert!(response.status.is_success());
783    }
784
785    #[tokio::test]
786    async fn test_http_client_delete() {
787        let addr = start_test_server().await.unwrap();
788        let url = format!("http://{addr}/delete");
789
790        let client = HttpClient::new(HashMap::new(), vec![], vec![], None, None, None).unwrap();
791        let response = client.delete(url, None, None, None, None).await.unwrap();
792
793        assert!(response.status.is_success());
794    }
795}