Skip to main content

zng_task/
http.rs

1#![cfg(feature = "http")]
2
3//! HTTP client.
4//!
5//! This module provides an HTTP client API that is backend agnostic. By default it uses the system `curl` command
6//! line utility with a simple cache, this can be replaced by implementing [`HttpClient`] and [`HttpCache`].
7//!
8
9mod cache;
10mod ctx;
11mod curl;
12mod file_cache;
13mod util;
14
15pub use cache::{CacheKey, CacheMode, CachePolicy};
16pub use ctx::{HttpCache, HttpClient, http_cache, http_client, set_http_cache, set_http_client, set_request_default};
17pub use curl::CurlProcessClient;
18pub use file_cache::FileSystemCache;
19
20/// Any error sending request or receiving response.
21///
22/// # HTTP Error
23///
24/// In functions that return `Result<Response, Error>` successfully received HTTP errors are not converted to `Err`. Use
25/// [`Response::error`] to get a [`HttpError`] if the response represents an error.
26///
27/// In helper functions that convert the response body to a value the [`HttpError`] is converted into this type. You
28/// can downcast to get the HTTP error in those cases.
29pub type Error = Box<dyn std::error::Error + Send + Sync>;
30
31pub use http::{
32    StatusCode, header,
33    method::{self, Method},
34    uri::{self, Uri},
35};
36use serde::{Deserialize, Serialize};
37use zng_var::{Var, const_var};
38
39use std::time::Duration;
40use std::{fmt, mem};
41
42use crate::{channel::IpcBytes, http::ctx::REQUEST_DEFAULT, io::Metrics};
43
44use super::io::AsyncRead;
45
46use zng_txt::{ToTxt, Txt, formatx};
47use zng_unit::*;
48
49/// HTTP request.
50///
51/// Use [`send`] to send a request.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[non_exhaustive]
54pub struct Request {
55    /// The URI.
56    #[serde(with = "http_serde::uri")]
57    pub uri: Uri,
58    /// The HTTP method.
59    #[serde(with = "http_serde::method")]
60    pub method: Method,
61
62    /// Header values.
63    ///
64    /// Is empty by default.
65    #[serde(with = "http_serde::header_map")]
66    pub headers: http::HeaderMap,
67
68    /// Maximum amount of time that a complete request/response cycle is allowed to
69    /// take before being aborted. This includes DNS resolution, connecting to the server,
70    /// writing the request, and reading the response.
71    ///
72    /// Note that this includes the response read operation, so if you get a response but don't
73    /// read-it within this timeout you will get a [`TimedOut`] IO error.
74    ///
75    /// By default no timeout is used, [`Duration::MAX`].
76    ///
77    /// [`TimedOut`]: https://doc.rust-lang.org/nightly/std/io/enum.ErrorKind.html#variant.TimedOut
78    pub timeout: Duration,
79
80    /// Maximum amount of time to await for establishing connections to a host.
81    ///
82    /// Is 90 seconds by default.
83    pub connect_timeout: Duration,
84
85    /// Maximum amount of time allowed when transfer speed is under the given speed in bytes per second.
86    ///
87    /// By default not timeout is used, `(Duration::MAX, 0)`.
88    pub low_speed_timeout: (Duration, ByteLength),
89
90    /// Maximum redirects to follow.
91    ///
92    /// When redirecting the `Referer` header is updated automatically.
93    ///
94    /// Is `20` by default.
95    pub redirect_limit: u16,
96
97    /// If should auto decompress received data.
98    ///
99    /// If enabled the "Accept-Encoding" will also be set automatically, if it was not set on the header.
100    ///
101    /// This is enabled by default.
102    #[cfg(feature = "http_compression")]
103    pub auto_decompress: bool,
104
105    /// Maximum upload speed in bytes per second.
106    ///
107    /// No maximum by default, [`ByteLength::MAX`].
108    pub max_upload_speed: ByteLength,
109
110    /// Maximum download speed in bytes per second.
111    ///
112    /// No maximum by default, [`ByteLength::MAX`].
113    pub max_download_speed: ByteLength,
114
115    /// If the `Content-Length` header must be present in the response.
116    ///
117    /// By default this is not required.
118    pub require_length: bool,
119
120    /// Set the maximum response content length allowed.
121    ///
122    /// If the `Content-Length` is present on the response and it exceeds this limit an error is
123    /// returned immediately, otherwise if [`require_length`] is not enabled an error will be returned
124    /// only when the downloaded body length exceeds the limit.
125    ///
126    /// By default no limit is set, [`ByteLength::MAX`].
127    ///
128    /// [`require_length`]: Request::require_length
129    pub max_length: ByteLength,
130
131    /// Response cache mode.
132    ///
133    /// Is [`CacheMode::Default`] by default.
134    pub cache: CacheMode,
135
136    /// If cookies should be send and stored.
137    ///
138    /// When enabled the [`http_cache`] is used to retrieve and store cookies.
139    ///
140    /// Is not enabled by default.
141    #[cfg(feature = "http_cookie")]
142    pub cookies: bool,
143
144    /// If transfer metrics should be measured.
145    ///
146    /// When enabled you can get the information using the [`Response::metrics`] method.
147    ///
148    /// This is enabled by default.
149    pub metrics: bool,
150
151    /// Request body content.
152    ///
153    /// Is empty by default.
154    pub body: IpcBytes,
155}
156impl Request {
157    /// Starts building a request.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use zng_task::http;
163    ///
164    /// # fn try_example() -> Result<(), http::Error> {
165    /// let request = http::Request::new(http::Method::PUT, "https://httpbin.org/put".try_into()?);
166    /// # Ok(()) }
167    /// ```
168    pub fn new(method: Method, uri: Uri) -> Self {
169        match REQUEST_DEFAULT.lock().clone() {
170            Some(mut r) => {
171                r.method = method;
172                r.uri = uri;
173                r
174            }
175            None => Self {
176                uri,
177                method,
178                require_length: false,
179                max_length: ByteLength::MAX,
180                headers: header::HeaderMap::new(),
181                timeout: Duration::MAX,
182                connect_timeout: 90.secs(),
183                low_speed_timeout: (Duration::MAX, 0.bytes()),
184                redirect_limit: 20,
185                #[cfg(feature = "http_compression")]
186                auto_decompress: true,
187                max_upload_speed: ByteLength::MAX,
188                max_download_speed: ByteLength::MAX,
189                cache: CacheMode::Default,
190                #[cfg(feature = "http_cookie")]
191                cookies: false,
192                metrics: true,
193                body: IpcBytes::default(),
194            },
195        }
196    }
197
198    /// Starts building a GET request.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use zng_task::http;
204    ///
205    /// # fn try_example() -> Result<(), http::Error> {
206    /// let get = http::Request::get("https://httpbin.org/get")?;
207    /// # Ok(()) }
208    /// ```
209    pub fn get<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
210        Ok(Self::new(Method::GET, uri.try_into()?))
211    }
212
213    /// Starts building a PUT request.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use zng_task::http;
219    ///
220    /// # fn try_example() -> Result<(), http::Error> {
221    /// let put = http::Request::put("https://httpbin.org/put")?.header("accept", "application/json")?;
222    /// # Ok(()) }
223    /// ```
224    pub fn put<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
225        Ok(Self::new(Method::PUT, uri.try_into()?))
226    }
227
228    /// Starts building a POST request.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use zng_task::http;
234    ///
235    /// # fn try_example() -> Result<(), http::Error> {
236    /// let post = http::Request::post("https://httpbin.org/post")?.header("accept", "application/json")?;
237    /// # Ok(()) }
238    /// ```
239    pub fn post<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
240        Ok(Self::new(Method::POST, uri.try_into()?))
241    }
242
243    /// Starts building a DELETE request.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use zng_task::http;
249    ///
250    /// # fn try_example() -> Result<(), http::Error> {
251    /// let delete = http::Request::delete("https://httpbin.org/delete")?.header("accept", "application/json")?;
252    /// # Ok(()) }
253    /// ```
254    pub fn delete<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
255        Ok(Self::new(Method::DELETE, uri.try_into()?))
256    }
257
258    /// Starts building a PATCH request.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use zng_task::http;
264    ///
265    /// # fn try_example() -> Result<(), http::Error> {
266    /// let patch = http::Request::patch("https://httpbin.org/patch")?.header("accept", "application/json")?;
267    /// # Ok(()) }
268    /// ```
269    pub fn patch<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
270        Ok(Self::new(Method::PATCH, uri.try_into()?))
271    }
272
273    /// Starts building a HEAD request.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use zng_task::http;
279    ///
280    /// # fn try_example() -> Result<(), http::Error> {
281    /// let head = http::Request::head("https://httpbin.org")?;
282    /// # Ok(()) }
283    /// ```
284    pub fn head<U: TryInto<Uri>>(uri: U) -> Result<Self, <U as TryInto<Uri>>::Error> {
285        Ok(Self::new(Method::HEAD, uri.try_into()?))
286    }
287
288    /// Appends a header to [`headers`] to this request.
289    ///
290    /// [`headers`]: field@Request::headers
291    pub fn header<K, V>(mut self, name: K, value: V) -> Result<Self, Error>
292    where
293        K: TryInto<header::HeaderName>,
294        V: TryInto<header::HeaderValue>,
295        Error: From<<K as TryInto<header::HeaderName>>::Error>,
296        Error: From<<V as TryInto<header::HeaderValue>>::Error>,
297    {
298        self.headers.insert(name.try_into()?, value.try_into()?);
299        Ok(self)
300    }
301
302    /// Set the [`timeout`].
303    ///
304    /// [`timeout`]: field@Request::timeout
305    pub fn timeout(mut self, timeout: Duration) -> Self {
306        self.timeout = timeout;
307        self
308    }
309
310    /// Set the [`connect_timeout`].
311    ///
312    /// [`connect_timeout`]: field@Request::connect_timeout
313    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
314        self.connect_timeout = timeout;
315        self
316    }
317
318    /// Set the [`low_speed_timeout`].
319    ///
320    /// [`low_speed_timeout`]: field@Request::low_speed_timeout
321    pub fn low_speed_timeout(mut self, timeout: Duration, bytes_per_sec: ByteLength) -> Self {
322        self.low_speed_timeout = (timeout, bytes_per_sec);
323        self
324    }
325
326    /// Set the [`redirect_limit`].
327    ///
328    /// [`redirect_limit`]: field@Request::redirect_limit
329    pub fn redirect_limit(mut self, count: u16) -> Self {
330        self.redirect_limit = count;
331        self
332    }
333
334    /// Set the [`auto_decompress`].
335    ///
336    /// [`auto_decompress`]: field@Request::auto_decompress
337    #[cfg(feature = "http_compression")]
338    pub fn auto_decompress(mut self, enabled: bool) -> Self {
339        self.auto_decompress = enabled;
340        self
341    }
342
343    /// Set [`require_length`].
344    ///
345    /// [`require_length`]: field@Request::require_length
346    pub fn require_length(mut self, enabled: bool) -> Self {
347        self.require_length = enabled;
348        self
349    }
350
351    /// Set [`max_length`].
352    ///
353    /// [`max_length`]: field@Request::max_length
354    pub fn max_length(mut self, max: ByteLength) -> Self {
355        self.max_length = max;
356        self
357    }
358
359    /// Set the [`max_upload_speed`].
360    ///
361    /// [`max_upload_speed`]: field@Request::max_upload_speed
362    pub fn max_upload_speed(mut self, bytes_per_sec: ByteLength) -> Self {
363        self.max_upload_speed = bytes_per_sec;
364        self
365    }
366
367    /// Set the [`max_download_speed`].
368    ///
369    /// [`max_download_speed`]: field@Request::max_download_speed
370    pub fn max_download_speed(mut self, bytes_per_sec: ByteLength) -> Self {
371        self.max_download_speed = bytes_per_sec;
372        self
373    }
374
375    /// Set the [`cookies`].
376    ///
377    /// [`cookies`]: field@Request::cookies
378    #[cfg(feature = "http_cookie")]
379    pub fn cookies(mut self, enable: bool) -> Self {
380        self.cookies = enable;
381        self
382    }
383
384    /// Set the [`metrics`].
385    ///
386    /// [`metrics`]: field@Request::metrics
387    pub fn metrics(mut self, enabled: bool) -> Self {
388        self.metrics = enabled;
389        self
390    }
391
392    /// Set the [`body`].
393    ///
394    /// [`body`]: field@Request::body
395    pub fn body(mut self, body: IpcBytes) -> Self {
396        self.body = body;
397        self
398    }
399
400    /// Set the [`body`] to a plain text UTF-8 payload.  Also sets the `Content-Type` header if it is not set.
401    ///
402    /// [`body`]: field@Request::body
403    pub fn body_text(mut self, body: &str) -> Result<Self, Error> {
404        if !self.headers.contains_key("Content-Type") {
405            self = self.header("Content-Type", "text/plain; charset=utf-8")?;
406        }
407        Ok(self.body(IpcBytes::from_slice_blocking(body.as_bytes())?))
408    }
409
410    /// Set the [`body`] to a JSON payload. Also sets the `Content-Type` header if it is not set.
411    ///
412    /// [`body`]: field@Request::body
413    pub fn body_json<T: Serialize>(mut self, body: &T) -> Result<Self, Error> {
414        if !self.headers.contains_key("Content-Type") {
415            self = self.header("Content-Type", "text/json; charset=utf-8")?;
416        }
417        let body = serde_json::to_vec(body)?;
418        Ok(self.body(IpcBytes::from_vec_blocking(body)?))
419    }
420}
421impl From<Request> for http::Request<IpcBytes> {
422    fn from(mut r: Request) -> Self {
423        let mut b = http::Request::builder().uri(mem::take(&mut r.uri)).method(r.method.clone());
424        if !r.headers.is_empty() {
425            *b.headers_mut().unwrap() = mem::take(&mut r.headers);
426        }
427        let body = mem::take(&mut r.body);
428        let b = b.extension(r);
429        b.body(body).unwrap()
430    }
431}
432impl From<http::Request<IpcBytes>> for Request {
433    fn from(value: http::Request<IpcBytes>) -> Self {
434        let (mut parts, body) = value.into_parts();
435        if let Some(mut r) = parts.extensions.remove::<Request>() {
436            r.method = parts.method;
437            r.uri = parts.uri;
438            r.headers = parts.headers;
439            r.body = body;
440            r
441        } else {
442            let mut r = Request::new(parts.method, parts.uri);
443            r.headers = parts.headers;
444            r.body = body;
445            r
446        }
447    }
448}
449
450/// HTTP response.
451pub struct Response {
452    status: StatusCode,
453    headers: header::HeaderMap,
454    effective_uri: Uri,
455    body: ResponseBody,
456    metrics: Var<Metrics>,
457}
458impl fmt::Debug for Response {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        f.debug_struct("Response")
461            .field("status", &self.status)
462            .field("effective_uri", &self.effective_uri)
463            .field("header", &self.headers)
464            .field("metrics", &self.metrics.get())
465            .finish_non_exhaustive()
466    }
467}
468enum ResponseBody {
469    Done { bytes: IpcBytes },
470    Read { read: Box<dyn AsyncRead + Send> },
471}
472impl Response {
473    /// New with body download pending or ongoing.
474    pub fn from_read(
475        status: StatusCode,
476        header: header::HeaderMap,
477        effective_uri: Uri,
478        metrics: Var<Metrics>,
479        read: Box<dyn AsyncRead + Send>,
480    ) -> Self {
481        Self {
482            status,
483            headers: header,
484            effective_uri,
485            metrics,
486            body: ResponseBody::Read { read },
487        }
488    }
489
490    /// New with body already downloaded.
491    pub fn from_done(status: StatusCode, mut headers: header::HeaderMap, effective_uri: Uri, metrics: Metrics, body: IpcBytes) -> Self {
492        if !headers.contains_key(header::CONTENT_LENGTH) {
493            headers.insert(header::CONTENT_LENGTH, body.len().into());
494        }
495        Self {
496            status,
497            headers,
498            effective_uri,
499            metrics: const_var(metrics),
500            body: ResponseBody::Done { bytes: body },
501        }
502    }
503
504    /// New with status and message body.
505    pub fn from_msg(status: StatusCode, msg: impl ToTxt) -> Self {
506        Self::from_done(
507            status,
508            header::HeaderMap::new(),
509            Uri::from_static("/"),
510            Metrics::zero(),
511            IpcBytes::from_slice_blocking(msg.to_txt().as_bytes()).unwrap(),
512        )
513    }
514
515    /// Returns the [`StatusCode`].
516    pub fn status(&self) -> StatusCode {
517        self.status
518    }
519
520    /// Returns a reference to the associated header field map.
521    pub fn header(&self) -> &header::HeaderMap {
522        &self.headers
523    }
524
525    /// Get the effective URI of this response. This value differs from the
526    /// original URI provided when making the request if at least one redirect
527    /// was followed.
528    pub fn effective_uri(&self) -> &Uri {
529        &self.effective_uri
530    }
531
532    /// Get the body bytes length if it is downloaded or `Content-Length` value if it is present in the headers.
533    pub fn content_len(&self) -> Option<ByteLength> {
534        match &self.body {
535            ResponseBody::Done { bytes, .. } => Some((bytes.len() as u64).bytes()),
536            ResponseBody::Read { .. } => {
537                let len = self
538                    .headers
539                    .get(header::CONTENT_LENGTH)?
540                    .to_str()
541                    .ok()?
542                    .parse::<u64>()
543                    .ok()?
544                    .bytes();
545                Some(len)
546            }
547        }
548    }
549
550    /// Receive the entire body.
551    pub async fn download(&mut self) -> Result<(), Error> {
552        if let ResponseBody::Done { .. } = &self.body {
553            return Ok(());
554        }
555
556        let downloader = match mem::replace(
557            &mut self.body,
558            ResponseBody::Done {
559                bytes: IpcBytes::default(),
560            },
561        ) {
562            ResponseBody::Read { read: downloader } => downloader,
563            ResponseBody::Done { .. } => unreachable!(),
564        };
565        let mut downloader = Box::into_pin(downloader);
566        let body = IpcBytes::from_read(downloader.as_mut()).await?;
567
568        self.body = ResponseBody::Done { bytes: body };
569
570        Ok(())
571    }
572
573    /// Download the full body and returns a reference to it.
574    pub async fn body(&mut self) -> Result<IpcBytes, Error> {
575        self.download().await?;
576        match &self.body {
577            ResponseBody::Done { bytes } => Ok(bytes.clone()),
578            ResponseBody::Read { .. } => unreachable!(),
579        }
580    }
581
582    /// Download the full body and returns it decoded to text.
583    pub async fn body_text(&mut self) -> Result<Txt, Error> {
584        let content_type = self
585            .headers
586            .get(header::CONTENT_TYPE)
587            .and_then(|value| value.to_str().ok())
588            .and_then(|value| value.parse::<mime::Mime>().ok());
589        let encoding_name = content_type
590            .as_ref()
591            .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
592            .unwrap_or("utf-8");
593
594        let bytes = self.body().await?;
595
596        let encoding = encoding_rs::Encoding::for_label(encoding_name.as_bytes()).unwrap_or(encoding_rs::UTF_8);
597        let (text, _, _) = encoding.decode(&bytes);
598        Ok(Txt::from_str(&text))
599    }
600
601    /// Download the full body and returns it decoded to JSON and deserialized to `O`.
602    pub async fn body_json<O>(&mut self) -> Result<O, Error>
603    where
604        O: serde::de::DeserializeOwned + std::marker::Unpin,
605    {
606        let bytes = self.body().await?;
607        let r = serde_json::from_slice(&bytes)?;
608        Ok(r)
609    }
610
611    /// Metrics for the task transfer, if it was enabled in the request.
612    pub fn metrics(&self) -> Var<Metrics> {
613        self.metrics.read_only()
614    }
615
616    /// Convert failed status to an error.
617    ///
618    /// If the [`status`] is not success this will attempt to download the body as text, otherwise
619    /// the body is not accessed.
620    ///
621    /// [`status`]: Response::status
622    pub async fn error(&mut self) -> Result<(), HttpError> {
623        if self.status.is_success() {
624            Ok(())
625        } else {
626            let body = match self.body_text().await {
627                Ok(b) => b,
628                Err(e) => formatx!("could not receive error body, {e}"),
629            };
630            Err(HttpError { status: self.status, body })
631        }
632    }
633}
634
635/// Represents an HTTP response error.
636///
637/// See [`Response::error`] for more details.
638#[derive(Debug, Clone, PartialEq)]
639#[non_exhaustive]
640pub struct HttpError {
641    /// The error code.
642    pub status: StatusCode,
643    /// The error body.
644    pub body: Txt,
645}
646
647impl HttpError {
648    /// New error.
649    pub fn new(status: StatusCode, body: Txt) -> Self {
650        Self { status, body }
651    }
652}
653/// Alternate writes the `body`.
654impl fmt::Display for HttpError {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        write!(f, "HTTP error {}", self.status.as_str())?;
657        if let Some(r) = self.status.canonical_reason() {
658            write!(f, " {r}")?;
659        }
660        if f.alternate() && self.body.is_empty() {
661            for line in self.body.lines() {
662                write!(f, "\n   {line}")?;
663            }
664        }
665        Ok(())
666    }
667}
668impl std::error::Error for HttpError {}
669
670/// Send a GET request to the `uri`.
671///
672/// The [`http_client`] is used to send the request.
673pub async fn get<U>(uri: U) -> Result<Response, Error>
674where
675    U: TryInto<Uri>,
676    Error: From<<U as TryInto<Uri>>::Error>,
677{
678    send(Request::get(uri)?).await
679}
680
681/// Send a GET request to the `uri` and read the response as a string.
682///
683/// The [`http_client`] is used to send the request.
684pub async fn get_txt<U>(uri: U) -> Result<Txt, Error>
685where
686    U: TryInto<Uri>,
687    Error: From<<U as TryInto<Uri>>::Error>,
688{
689    let mut r = send(Request::get(uri)?).await?;
690    r.error().await?;
691    r.body_text().await
692}
693
694/// Send a GET request to the `uri` and read the response as raw bytes.
695///
696/// The [`http_client`] is used to send the request.
697pub async fn get_bytes<U>(uri: U) -> Result<IpcBytes, Error>
698where
699    U: TryInto<Uri>,
700    Error: From<<U as TryInto<Uri>>::Error>,
701{
702    let mut r = send(Request::get(uri)?).await?;
703    r.error().await?;
704    r.body().await
705}
706
707/// Send a GET request to the `uri` and de-serializes the response.
708///
709/// The [`http_client`] is used to send the request.
710pub async fn get_json<U, O>(uri: U) -> Result<O, Error>
711where
712    U: TryInto<Uri>,
713    Error: From<<U as TryInto<Uri>>::Error>,
714    O: serde::de::DeserializeOwned + std::marker::Unpin,
715{
716    let mut r = send(Request::get(uri)?).await?;
717    r.error().await?;
718    r.body_json().await
719}
720
721/// Send a HEAD request to the `uri`.
722///
723/// The [`http_client`] is used to send the request.
724pub async fn head<U>(uri: U) -> Result<Response, Error>
725where
726    U: TryInto<Uri>,
727    Error: From<<U as TryInto<Uri>>::Error>,
728{
729    send(Request::head(uri)?).await
730}
731
732/// Send a PUT request to the `uri` with a given request body.
733///
734/// The [`http_client`] is used to send the request.
735pub async fn put<U>(uri: U, body: IpcBytes) -> Result<Response, Error>
736where
737    U: TryInto<Uri>,
738    Error: From<<U as TryInto<Uri>>::Error>,
739{
740    send(Request::put(uri)?.body(body)).await
741}
742
743/// Send a POST request to the `uri` with a given request body.
744///
745/// The [`http_client`] is used to send the request.
746pub async fn post<U>(uri: U, body: IpcBytes) -> Result<Response, Error>
747where
748    U: TryInto<Uri>,
749    Error: From<<U as TryInto<Uri>>::Error>,
750{
751    send(Request::post(uri)?.body(body)).await
752}
753
754/// Send a DELETE request to the `uri`.
755///
756/// The [`http_client`] is used to send the request.
757pub async fn delete<U>(uri: U) -> Result<Response, Error>
758where
759    U: TryInto<Uri>,
760    Error: From<<U as TryInto<Uri>>::Error>,
761{
762    send(Request::delete(uri)?).await
763}
764
765/// Send a custom [`Request`].
766///
767/// The [`http_client`] is used to send the request.
768pub async fn send(request: Request) -> Result<Response, Error> {
769    let client = http_client();
770    if client.is_cache_manager() {
771        client.send(request).await
772    } else {
773        match request.cache {
774            CacheMode::NoCache => client.send(request).await,
775            CacheMode::Default => cache::send_cache(client, request).await,
776            CacheMode::Permanent => cache::send_cache_perm(client, request).await,
777        }
778    }
779}