Skip to main content

better_fetch/backend/
mod.rs

1//! HTTP transport abstraction.
2//!
3//! The default backend is [`ReqwestBackend`]. Inject a custom [`HttpBackend`] via
4//! [`ClientBuilder::backend`](crate::ClientBuilder::backend) for tests or alternate transports.
5//!
6//! Buffered responses use [`HttpBackend::execute`]. Streaming responses use
7//! [`HttpBackend::execute_stream`] (implemented by [`ReqwestBackend`] by default).
8
9pub(crate) mod exec;
10mod recording;
11mod reqwest;
12
13pub use recording::{RecordedBodyKind, RecordedRequest, RecordingBackend};
14pub use reqwest::ReqwestBackend;
15
16use async_trait::async_trait;
17use bytes::Bytes;
18use http::{HeaderMap, Method, StatusCode};
19use std::time::Duration;
20
21use crate::cancel::CancellationToken;
22use crate::streaming::BodyStream;
23use crate::Result;
24
25/// Returns `true` when the body cannot be sent again on retry.
26pub(crate) fn body_is_non_replayable(body: &HttpBody) -> bool {
27    matches!(body, HttpBody::Stream(_))
28}
29
30#[cfg(feature = "multipart")]
31use crate::multipart::Form as MultipartForm;
32
33/// Request body encoding for the transport layer.
34#[derive(Default)]
35pub enum HttpBody {
36    /// No body.
37    #[default]
38    Empty,
39    /// Raw bytes body.
40    Bytes(Bytes),
41    /// Streaming request body (not replayable for automatic retry).
42    Stream(BodyStream),
43}
44
45impl std::fmt::Debug for HttpBody {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Empty => write!(f, "Empty"),
49            Self::Bytes(b) => f.debug_tuple("Bytes").field(b).finish(),
50            Self::Stream(_) => write!(f, "Stream"),
51        }
52    }
53}
54
55impl Clone for HttpBody {
56    /// Clones empty and byte bodies. **Streaming bodies cannot be cloned** and become [`HttpBody::Empty`];
57    /// use move semantics or [`HttpRequest`] without cloning when the body is a stream.
58    fn clone(&self) -> Self {
59        match self {
60            Self::Empty => Self::Empty,
61            Self::Bytes(b) => Self::Bytes(b.clone()),
62            Self::Stream(_) => {
63                debug_assert!(
64                    false,
65                    "HttpBody::Stream must not be cloned; body was replaced with Empty"
66                );
67                Self::Empty
68            }
69        }
70    }
71}
72
73/// Prepared HTTP request passed to a backend.
74#[derive(Debug)]
75pub struct HttpRequest {
76    /// HTTP method.
77    pub method: Method,
78    /// Fully resolved URL.
79    pub url: url::Url,
80    /// Request headers.
81    pub headers: HeaderMap,
82    /// Body when not using multipart.
83    pub body: HttpBody,
84    /// Per-request timeout.
85    pub timeout: Option<Duration>,
86    /// Cooperative cancellation.
87    pub cancellation: Option<CancellationToken>,
88    #[cfg(feature = "multipart")]
89    /// Multipart form (feature `multipart`).
90    pub multipart: Option<MultipartForm>,
91}
92
93impl Clone for HttpRequest {
94    fn clone(&self) -> Self {
95        Self {
96            method: self.method.clone(),
97            url: self.url.clone(),
98            headers: self.headers.clone(),
99            body: self.body.clone(),
100            timeout: self.timeout,
101            cancellation: self.cancellation.clone(),
102            #[cfg(feature = "multipart")]
103            multipart: None,
104        }
105    }
106}
107
108/// Raw HTTP response from a backend (fully buffered).
109#[derive(Debug, Clone)]
110pub struct HttpResponse {
111    /// HTTP status.
112    pub status: StatusCode,
113    /// Response headers.
114    pub headers: HeaderMap,
115    /// Response body bytes.
116    pub body: Bytes,
117}
118
119/// Raw HTTP response with a streaming body from a backend.
120pub struct HttpStreamingResponse {
121    /// HTTP status.
122    pub status: StatusCode,
123    /// Response headers.
124    pub headers: HeaderMap,
125    /// Response body stream.
126    pub body: BodyStream,
127}
128
129impl std::fmt::Debug for HttpStreamingResponse {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("HttpStreamingResponse")
132            .field("status", &self.status)
133            .field("headers", &self.headers)
134            .field("body", &"<stream>")
135            .finish()
136    }
137}
138
139/// Pluggable HTTP transport used by [`Client`](crate::Client).
140#[async_trait]
141pub trait HttpBackend: Send + Sync {
142    /// Executes one HTTP request and returns the fully buffered response.
143    async fn execute(&self, request: HttpRequest) -> Result<HttpResponse>;
144
145    /// Executes one HTTP request and returns a streaming response body.
146    async fn execute_stream(&self, request: HttpRequest) -> Result<HttpStreamingResponse>;
147}