Skip to main content

specter/
request.rs

1//! Request and body types with reqwest-like ergonomics.
2
3use crate::error::{Error, Result};
4use crate::headers::Headers;
5use crate::url::Url;
6use crate::version::HttpVersion;
7use bytes::Bytes;
8use futures_core::Stream;
9use http::Method;
10use std::fmt;
11use std::pin::Pin;
12use std::time::Duration;
13
14/// Convert common URL inputs into a `Url`.
15pub trait IntoUrl {
16    fn into_url(self) -> Result<Url>;
17}
18
19impl IntoUrl for Url {
20    fn into_url(self) -> Result<Url> {
21        Ok(self)
22    }
23}
24
25impl IntoUrl for &Url {
26    fn into_url(self) -> Result<Url> {
27        Ok(self.clone())
28    }
29}
30
31impl IntoUrl for &str {
32    fn into_url(self) -> Result<Url> {
33        Url::parse(self).map_err(Error::from)
34    }
35}
36
37impl IntoUrl for String {
38    fn into_url(self) -> Result<Url> {
39        Url::parse(&self).map_err(Error::from)
40    }
41}
42
43impl IntoUrl for &String {
44    fn into_url(self) -> Result<Url> {
45        Url::parse(self).map_err(Error::from)
46    }
47}
48
49impl IntoUrl for http::Uri {
50    fn into_url(self) -> Result<Url> {
51        Url::parse(&self.to_string()).map_err(Error::from)
52    }
53}
54
55impl IntoUrl for &http::Uri {
56    fn into_url(self) -> Result<Url> {
57        Url::parse(&self.to_string()).map_err(Error::from)
58    }
59}
60
61/// Boxed streaming producer of request body chunks.
62pub type RequestBodyStream =
63    Pin<Box<dyn Stream<Item = std::result::Result<Bytes, Error>> + Send + 'static>>;
64
65/// Public request body model.
66///
67/// Streaming variants are non-cloneable and cannot be replayed implicitly.
68/// Redirect/retry paths must fail closed when replay would be required instead
69/// of cloning a stream into an empty request body. Cloning of in-memory
70/// variants remains cheap.
71#[derive(Default)]
72pub enum RequestBody {
73    #[default]
74    Empty,
75    Bytes(Bytes),
76    Text(String),
77    Json(Vec<u8>),
78    Form(String),
79    Stream {
80        stream: RequestBodyStream,
81        content_length: Option<u64>,
82    },
83}
84
85impl RequestBody {
86    pub fn empty() -> Self {
87        RequestBody::Empty
88    }
89
90    pub fn is_empty(&self) -> bool {
91        matches!(self, RequestBody::Empty)
92    }
93
94    /// `true` when the body is a non-materialized streaming producer.
95    pub fn is_streaming(&self) -> bool {
96        matches!(self, RequestBody::Stream { .. })
97    }
98
99    /// Advertised `Content-Length` for sized streams (and trivially the
100    /// in-memory length for materialized variants).
101    pub fn content_length(&self) -> Option<u64> {
102        match self {
103            RequestBody::Empty => Some(0),
104            RequestBody::Bytes(b) => Some(b.len() as u64),
105            RequestBody::Text(t) => Some(t.len() as u64),
106            RequestBody::Json(b) => Some(b.len() as u64),
107            RequestBody::Form(t) => Some(t.len() as u64),
108            RequestBody::Stream { content_length, .. } => *content_length,
109        }
110    }
111
112    /// Materialize an in-memory body to [`Bytes`]. Streaming bodies fail
113    /// closed with a clear error rather than being silently buffered.
114    pub fn into_bytes(self) -> Result<Bytes> {
115        Ok(match self {
116            RequestBody::Empty => Bytes::new(),
117            RequestBody::Bytes(bytes) => bytes,
118            RequestBody::Text(text) => Bytes::from(text.into_bytes()),
119            RequestBody::Json(bytes) => Bytes::from(bytes),
120            RequestBody::Form(text) => Bytes::from(text.into_bytes()),
121            RequestBody::Stream { .. } => {
122                return Err(Error::HttpProtocol(
123                    "streaming RequestBody cannot be materialized; use the streaming send path"
124                        .into(),
125                ));
126            }
127        })
128    }
129}
130
131impl fmt::Debug for RequestBody {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            RequestBody::Empty => f.debug_struct("RequestBody::Empty").finish(),
135            RequestBody::Bytes(b) => f
136                .debug_struct("RequestBody::Bytes")
137                .field("len", &b.len())
138                .finish(),
139            RequestBody::Text(t) => f
140                .debug_struct("RequestBody::Text")
141                .field("len", &t.len())
142                .finish(),
143            RequestBody::Json(b) => f
144                .debug_struct("RequestBody::Json")
145                .field("len", &b.len())
146                .finish(),
147            RequestBody::Form(t) => f
148                .debug_struct("RequestBody::Form")
149                .field("len", &t.len())
150                .finish(),
151            RequestBody::Stream { content_length, .. } => f
152                .debug_struct("RequestBody::Stream")
153                .field("content_length", content_length)
154                .finish(),
155        }
156    }
157}
158
159impl Clone for RequestBody {
160    fn clone(&self) -> Self {
161        match self {
162            RequestBody::Empty => RequestBody::Empty,
163            RequestBody::Bytes(b) => RequestBody::Bytes(b.clone()),
164            RequestBody::Text(t) => RequestBody::Text(t.clone()),
165            RequestBody::Json(b) => RequestBody::Json(b.clone()),
166            RequestBody::Form(t) => RequestBody::Form(t.clone()),
167            RequestBody::Stream { .. } => {
168                panic!("RequestBody::Stream cannot be cloned or replayed")
169            }
170        }
171    }
172}
173
174impl From<Bytes> for RequestBody {
175    fn from(value: Bytes) -> Self {
176        RequestBody::Bytes(value)
177    }
178}
179
180impl From<Vec<u8>> for RequestBody {
181    fn from(value: Vec<u8>) -> Self {
182        RequestBody::Bytes(Bytes::from(value))
183    }
184}
185
186impl From<&[u8]> for RequestBody {
187    fn from(value: &[u8]) -> Self {
188        RequestBody::Bytes(Bytes::copy_from_slice(value))
189    }
190}
191
192impl From<String> for RequestBody {
193    fn from(value: String) -> Self {
194        RequestBody::Text(value)
195    }
196}
197
198impl From<&str> for RequestBody {
199    fn from(value: &str) -> Self {
200        RequestBody::Text(value.to_string())
201    }
202}
203
204/// High-level request object for execution.
205#[derive(Clone, Debug)]
206pub struct Request {
207    pub(crate) method: Method,
208    pub(crate) url: Url,
209    pub(crate) headers: Headers,
210    pub(crate) body: RequestBody,
211    pub(crate) version: Option<HttpVersion>,
212    pub(crate) timeout: Option<Duration>,
213}
214
215impl Request {
216    pub fn new(method: Method, url: Url) -> Self {
217        Self {
218            method,
219            url,
220            headers: Headers::new(),
221            body: RequestBody::Empty,
222            version: None,
223            timeout: None,
224        }
225    }
226
227    pub fn method(&self) -> &Method {
228        &self.method
229    }
230
231    pub fn url(&self) -> &Url {
232        &self.url
233    }
234
235    pub fn headers(&self) -> &Headers {
236        &self.headers
237    }
238
239    pub fn body(&self) -> &RequestBody {
240        &self.body
241    }
242
243    pub fn version(&self) -> Option<HttpVersion> {
244        self.version
245    }
246
247    pub fn timeout(&self) -> Option<Duration> {
248        self.timeout
249    }
250}
251
252/// Redirect policy for the client.
253#[derive(Clone, Debug, Default)]
254pub enum RedirectPolicy {
255    #[default]
256    None,
257    Limited(u32),
258}