Skip to main content

crux_http/
request.rs

1use crate::{Result, body::Body, middleware::Middleware};
2use http::{HeaderMap, HeaderName, HeaderValue, Method};
3use serde::Serialize;
4use std::{fmt, ops::Index, sync::Arc};
5use url::Url;
6
7/// An HTTP request, returns a `Response`.
8#[derive(Clone)]
9pub struct Request {
10    method: Method,
11    url: Url,
12    headers: HeaderMap,
13    body: Body,
14    middleware: Option<Vec<Arc<dyn Middleware>>>,
15}
16
17impl Request {
18    /// Create a new instance.
19    ///
20    /// This method is particularly useful when input URLs might be passed by third parties, and
21    /// you don't want to panic if they're malformed. If URLs are statically encoded, it might be
22    /// easier to use one of the shorthand methods instead.
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// fn main() -> crux_http::Result<()> {
28    /// use crux_http::{Url, Method};
29    ///
30    /// let url = Url::parse("https://httpbin.org/get")?;
31    /// let req = crux_http::Request::new(Method::GET, url);
32    /// # Ok(()) }
33    /// ```
34    #[must_use]
35    pub fn new(method: Method, url: Url) -> Self {
36        Self {
37            method,
38            url,
39            headers: HeaderMap::new(),
40            body: Body::default(),
41            middleware: None,
42        }
43    }
44
45    /// Get the URL querystring.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// fn main() -> crux_http::Result<()> {
51    /// use serde::{Deserialize, Serialize};
52    /// use crux_http::{Request, Method, Url};
53    /// #[derive(Serialize, Deserialize)]
54    /// struct Index {
55    ///     page: u32
56    /// }
57    ///
58    /// let req = Request::new(Method::GET, Url::parse("https://httpbin.org/get?page=2")?);
59    /// let Index { page } = req.query()?;
60    /// assert_eq!(page, 2);
61    /// # Ok(()) }
62    /// ```
63    ///
64    /// # Errors
65    /// Returns an error if the query string could not be deserialized.
66    pub fn query<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
67        let query = self.url.query().unwrap_or("");
68        serde_qs::from_str(query).map_err(Into::into)
69    }
70
71    /// Set the URL querystring.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// fn main() -> crux_http::Result<()> {
77    /// # use serde::{Deserialize, Serialize};
78    /// # use crux_http::{Request, Method, Url};
79    /// #[derive(Serialize, Deserialize)]
80    /// struct Index {
81    ///     page: u32
82    /// }
83    ///
84    /// let query = Index { page: 2 };
85    /// let mut req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
86    /// req.set_query(&query)?;
87    /// assert_eq!(req.url().query(), Some("page=2"));
88    /// assert_eq!(req.url().as_str(), "https://httpbin.org/get?page=2");
89    /// # Ok(()) }
90    /// ```
91    ///
92    /// # Errors
93    /// Returns an error if the query string could not be serialized.
94    pub fn set_query(&mut self, query: &impl Serialize) -> Result<()> {
95        let qs = serde_qs::to_string(query)?;
96        self.url.set_query(Some(&qs));
97        Ok(())
98    }
99
100    /// Get an HTTP header.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// fn main() -> crux_http::Result<()> {
106    /// # use crux_http::{Request, Method, Url};
107    /// # use crux_http::http::HeaderValue;
108    /// let mut req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
109    /// req.insert_header("X-Requested-With", HeaderValue::from_static("surf"));
110    /// assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
111    /// # Ok(()) }
112    /// ```
113    pub fn header(&self, name: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
114        self.headers.get(name)
115    }
116
117    /// Get a mutable reference to a header.
118    pub fn header_mut(
119        &mut self,
120        name: impl http::header::AsHeaderName,
121    ) -> Option<&mut HeaderValue> {
122        self.headers.get_mut(name)
123    }
124
125    /// Get all values for a header name.
126    pub fn header_all(
127        &self,
128        name: impl http::header::AsHeaderName,
129    ) -> http::header::GetAll<'_, HeaderValue> {
130        self.headers.get_all(name)
131    }
132
133    /// Set an HTTP header, replacing any existing value.
134    ///
135    /// Returns the previous value for that header name, if any.
136    pub fn insert_header(
137        &mut self,
138        name: impl http::header::IntoHeaderName,
139        value: HeaderValue,
140    ) -> Option<HeaderValue> {
141        self.headers.insert(name, value)
142    }
143
144    /// Append a header to the headers.
145    ///
146    /// Unlike `insert_header` this function will not override the contents of a header, but insert
147    /// a header if there aren't any. Or else append to the existing list of headers.
148    ///
149    /// Returns `true` if the value was appended to an existing entry, `false` if it was the first
150    /// value for that name.
151    pub fn append_header(
152        &mut self,
153        name: impl http::header::IntoHeaderName,
154        value: HeaderValue,
155    ) -> bool {
156        self.headers.append(name, value)
157    }
158
159    /// Remove a header.
160    pub fn remove_header(&mut self, name: impl http::header::AsHeaderName) -> Option<HeaderValue> {
161        self.headers.remove(name)
162    }
163
164    /// An iterator visiting all header pairs in arbitrary order.
165    #[must_use]
166    pub fn iter(&self) -> http::header::Iter<'_, HeaderValue> {
167        self.headers.iter()
168    }
169
170    /// An iterator visiting all header pairs in arbitrary order, with mutable references to the
171    /// values.
172    #[must_use]
173    pub fn iter_mut(&mut self) -> http::header::IterMut<'_, HeaderValue> {
174        self.headers.iter_mut()
175    }
176
177    /// An iterator visiting all header names in arbitrary order.
178    #[must_use]
179    pub fn header_names(&self) -> http::header::Keys<'_, HeaderValue> {
180        self.headers.keys()
181    }
182
183    /// An iterator visiting all header values in arbitrary order.
184    #[must_use]
185    pub fn header_values(&self) -> http::header::Values<'_, HeaderValue> {
186        self.headers.values()
187    }
188
189    /// Set an HTTP header.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// fn main() -> crux_http::Result<()> {
195    /// # use crux_http::{Request, Method, Url};
196    /// # use crux_http::http::HeaderValue;
197    /// let mut req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
198    /// req.insert_header("X-Requested-With", HeaderValue::from_static("surf"));
199    /// assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
200    /// # Ok(()) }
201    /// ```
202    #[deprecated(since = "0.16.0", note = "Use `insert_header` instead")]
203    pub fn set_header(&mut self, key: impl http::header::IntoHeaderName, value: impl AsRef<str>) {
204        if let Ok(v) = HeaderValue::from_str(value.as_ref()) {
205            self.insert_header(key, v);
206        }
207    }
208
209    /// Get the request HTTP method.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// fn main() -> crux_http::Result<()> {
215    /// # use crux_http::{Request, Method, Url};
216    /// let req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
217    /// assert_eq!(req.method(), &Method::GET);
218    /// # Ok(()) }
219    /// ```
220    #[must_use]
221    #[allow(clippy::missing_const_for_fn)]
222    pub fn method(&self) -> &Method {
223        &self.method
224    }
225
226    /// Get the request url.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// fn main() -> crux_http::Result<()> {
232    /// # use crux_http::{Request, Method, Url};
233    /// let req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
234    /// assert_eq!(req.url(), &Url::parse("https://httpbin.org/get")?);
235    /// # Ok(()) }
236    /// ```
237    #[must_use]
238    #[allow(clippy::missing_const_for_fn)]
239    pub fn url(&self) -> &Url {
240        &self.url
241    }
242
243    /// Get a mutable reference to the request url.
244    ///
245    /// This is useful for middleware that needs to rewrite the request URL.
246    #[must_use]
247    #[allow(clippy::missing_const_for_fn)]
248    pub fn url_mut(&mut self) -> &mut Url {
249        &mut self.url
250    }
251
252    /// Get the request content type as a `Mime`.
253    ///
254    /// Gets the `Content-Type` header and parses it to a `Mime` type.
255    ///
256    /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
257    ///
258    /// # Panics
259    ///
260    /// This method will panic if an invalid MIME type was set as a header. Use the [`set_header`]
261    /// method to bypass any checks.
262    ///
263    /// [`set_header`]: #method.set_header
264    #[must_use]
265    pub fn content_type(&self) -> Option<mime::Mime> {
266        self.headers
267            .get(http::header::CONTENT_TYPE)?
268            .to_str()
269            .ok()?
270            .parse()
271            .ok()
272    }
273
274    /// Set the request content type from a `Mime`.
275    ///
276    /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
277    pub fn set_content_type(&mut self, mime: &mime::Mime) {
278        if let Ok(v) = HeaderValue::from_str(mime.as_ref()) {
279            self.headers.insert(http::header::CONTENT_TYPE, v);
280        }
281    }
282
283    /// Get the length of the body stream, if it has been set.
284    ///
285    /// This value is set when passing a fixed-size object into as the body.
286    /// E.g. a string, or a buffer. Consumers of this API should check this
287    /// value to decide whether to use `Chunked` encoding, or set the
288    /// response length.
289    #[allow(clippy::len_without_is_empty)]
290    #[must_use]
291    pub fn len(&self) -> Option<usize> {
292        Some(self.body.len())
293    }
294
295    /// Returns `true` if the set length of the body stream is zero, `false`
296    /// otherwise.
297    #[must_use]
298    pub fn is_empty(&self) -> Option<bool> {
299        Some(self.body.is_empty())
300    }
301
302    /// Pass an `AsyncRead` stream as the request body.
303    ///
304    /// # Mime
305    ///
306    /// The encoding is set to `application/octet-stream`.
307    pub fn set_body(&mut self, body: impl Into<Body>) {
308        let body = body.into();
309        if let Some(mime) = body.mime()
310            && let Ok(v) = HeaderValue::from_str(mime.as_ref())
311        {
312            self.headers.insert(http::header::CONTENT_TYPE, v);
313        }
314        self.body = body;
315    }
316
317    /// Take the request body as a `Body`.
318    ///
319    /// This method can be called after the body has already been taken or read,
320    /// but will return an empty `Body`.
321    ///
322    /// This is useful for consuming the body via an `AsyncReader` or `AsyncBufReader`.
323    pub fn take_body(&mut self) -> Body {
324        std::mem::take(&mut self.body)
325    }
326
327    /// Pass JSON as the request body.
328    ///
329    /// # Mime
330    ///
331    /// The `content-type` is set to `application/json`.
332    ///
333    /// # Errors
334    ///
335    /// This method will return an error if the provided data could not be serialized to JSON.
336    pub fn body_json(&mut self, json: &impl Serialize) -> Result<()> {
337        self.set_body(Body::from_json(json)?);
338        Ok(())
339    }
340
341    /// Pass a string as the request body.
342    ///
343    /// # Mime
344    ///
345    /// The `content-type` is set to `text/plain; charset=utf-8`.
346    pub fn body_string(&mut self, string: String) {
347        self.set_body(Body::from_string(string));
348    }
349
350    /// Pass bytes as the request body.
351    ///
352    /// # Mime
353    ///
354    /// The `content-type` is set to `application/octet-stream`.
355    pub fn body_bytes(&mut self, bytes: impl AsRef<[u8]>) {
356        self.set_body(Body::from(bytes.as_ref()));
357    }
358
359    /// Pass a form as the request body.
360    ///
361    /// # Mime
362    ///
363    /// The `content-type` is set to `application/x-www-form-urlencoded`.
364    ///
365    /// # Errors
366    ///
367    /// An error will be returned if the encoding failed.
368    pub fn body_form(&mut self, form: &impl Serialize) -> Result<()> {
369        self.set_body(Body::from_form(form)?);
370        Ok(())
371    }
372
373    /// Push middleware onto a per-request middleware stack.
374    ///
375    /// **Important**: Setting per-request middleware incurs extra allocations.
376    /// Creating a `Client` with middleware is recommended.
377    ///
378    /// Client middleware is run before per-request middleware.
379    ///
380    /// See the [middleware] submodule for more information on middleware.
381    ///
382    /// [middleware]: ../middleware/index.html
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// fn main() -> crux_http::Result<()> {
388    /// # use crux_http::{Request, Method, Url};
389    /// let mut req = Request::new(Method::GET, Url::parse("https://httpbin.org/get")?);
390    /// req.middleware(crux_http::middleware::Redirect::default());
391    /// # Ok(()) }
392    /// ```
393    #[allow(clippy::missing_panics_doc)]
394    pub fn middleware(&mut self, middleware: impl Middleware) {
395        if self.middleware.is_none() {
396            self.middleware = Some(vec![]);
397        }
398
399        self.middleware.as_mut().unwrap().push(Arc::new(middleware));
400    }
401
402    pub(crate) fn take_middleware(&mut self) -> Option<Vec<Arc<dyn Middleware>>> {
403        self.middleware.take()
404    }
405}
406
407impl AsRef<HeaderMap> for Request {
408    fn as_ref(&self) -> &HeaderMap {
409        &self.headers
410    }
411}
412
413impl AsMut<HeaderMap> for Request {
414    fn as_mut(&mut self) -> &mut HeaderMap {
415        &mut self.headers
416    }
417}
418
419impl From<http::Request<Body>> for Request {
420    fn from(req: http::Request<Body>) -> Self {
421        let (parts, body) = req.into_parts();
422        let url = parts
423            .uri
424            .to_string()
425            .parse()
426            .unwrap_or_else(|_| Url::parse("https://invalid.example.com").unwrap());
427        Self {
428            method: parts.method,
429            url,
430            headers: parts.headers,
431            body,
432            middleware: None,
433        }
434    }
435}
436
437impl From<Request> for http::Request<Body> {
438    fn from(req: Request) -> Self {
439        let mut builder = http::Request::builder()
440            .method(req.method)
441            .uri(req.url.as_str());
442        for (name, value) in &req.headers {
443            builder = builder.header(name, value);
444        }
445        builder.body(req.body).expect("valid request")
446    }
447}
448
449impl fmt::Debug for Request {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        f.debug_struct("Request")
452            .field("method", &self.method)
453            .field("url", &self.url.as_str())
454            .finish_non_exhaustive()
455    }
456}
457
458impl<'a> IntoIterator for &'a Request {
459    type Item = (&'a HeaderName, &'a HeaderValue);
460    type IntoIter = http::header::Iter<'a, HeaderValue>;
461
462    #[inline]
463    fn into_iter(self) -> Self::IntoIter {
464        self.headers.iter()
465    }
466}
467
468impl<'a> IntoIterator for &'a mut Request {
469    type Item = (&'a HeaderName, &'a mut HeaderValue);
470    type IntoIter = http::header::IterMut<'a, HeaderValue>;
471
472    #[inline]
473    fn into_iter(self) -> Self::IntoIter {
474        self.headers.iter_mut()
475    }
476}
477
478impl Index<&str> for Request {
479    type Output = HeaderValue;
480
481    /// Returns a reference to the value corresponding to the supplied name.
482    ///
483    /// # Panics
484    ///
485    /// Panics if the name is not present in `Request`.
486    #[inline]
487    fn index(&self, name: &str) -> &HeaderValue {
488        &self.headers[name]
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    fn get(url: &str) -> Request {
497        Request::new(Method::GET, Url::parse(url).unwrap())
498    }
499
500    #[test]
501    fn new_request_has_empty_body() {
502        let req = get("https://example.com");
503        assert!(req.is_empty() == Some(true));
504        assert_eq!(req.len(), Some(0));
505    }
506
507    #[test]
508    fn set_body_string_stores_bytes_and_content_type_header() {
509        let mut req = get("https://example.com");
510        req.set_body("hello");
511        assert_eq!(req.len(), Some(5));
512        // Content-Type header must be propagated to the headers map
513        // so that iter() picks it up when building HttpRequest headers.
514        let ct = req.content_type().expect("content type must be set");
515        assert_eq!(ct, mime::TEXT_PLAIN_UTF_8);
516    }
517
518    #[test]
519    fn set_body_bytes_sets_octet_stream_content_type() {
520        let mut req = get("https://example.com");
521        req.set_body(vec![1u8, 2, 3]);
522        let ct = req.content_type().expect("content type must be set");
523        assert_eq!(ct, mime::APPLICATION_OCTET_STREAM);
524    }
525
526    #[test]
527    fn take_body_empties_body() {
528        let mut req = get("https://example.com");
529        req.set_body("world");
530        let body = req.take_body();
531        assert_eq!(body.into_bytes(), b"world");
532        assert!(req.is_empty() == Some(true));
533    }
534
535    #[test]
536    fn body_json_sets_application_json_content_type() {
537        let mut req = get("https://example.com");
538        req.body_json(&serde_json::json!({"key": "val"})).unwrap();
539        let ct = req.content_type().expect("content type must be set");
540        assert_eq!(ct, mime::APPLICATION_JSON);
541        assert!(!req.is_empty().unwrap());
542    }
543
544    #[test]
545    fn body_form_sets_form_urlencoded_content_type() {
546        #[derive(serde::Serialize)]
547        struct F {
548            a: u32,
549        }
550        let mut req = get("https://example.com");
551        req.body_form(&F { a: 1 }).unwrap();
552        let ct = req.content_type().expect("content type must be set");
553        assert_eq!(ct, mime::APPLICATION_WWW_FORM_URLENCODED);
554    }
555
556    #[test]
557    fn method_is_http_method() {
558        let req = Request::new(Method::POST, Url::parse("https://example.com").unwrap());
559        assert_eq!(req.method(), &Method::POST);
560    }
561
562    #[test]
563    fn from_http_request_roundtrip() {
564        use crate::Body;
565        let http_req = http::Request::builder()
566            .method(Method::PUT)
567            .uri("https://example.com/path")
568            .header("x-test", "value")
569            .body(Body::from("payload"))
570            .unwrap();
571
572        let req: Request = http_req.into();
573        assert_eq!(req.method(), &Method::PUT);
574        assert_eq!(req.header("x-test").unwrap().to_str().unwrap(), "value");
575        assert_eq!(req.len(), Some(7));
576
577        // And back again
578        let back: http::Request<Body> = req.into();
579        assert_eq!(back.method(), Method::PUT);
580        assert_eq!(back.headers()["x-test"], "value");
581    }
582
583    #[test]
584    fn from_http_request_with_unparseable_uri_falls_back_to_placeholder() {
585        // `http::Uri` accepts asterisk-form (`*`, used in OPTIONS) which `url::Url`
586        // cannot parse.  The impl falls back to a placeholder URL rather than panicking.
587        use crate::Body;
588        let http_req = http::Request::builder()
589            .method(Method::OPTIONS)
590            .uri("*")
591            .body(Body::from(vec![]))
592            .unwrap();
593        let req: Request = http_req.into();
594        assert_eq!(req.url().as_str(), "https://invalid.example.com/");
595    }
596
597    #[test]
598    fn url_mut_allows_url_mutation() {
599        let mut req = get("https://example.com/old");
600        *req.url_mut() = Url::parse("https://example.com/new").unwrap();
601        assert_eq!(req.url().as_str(), "https://example.com/new");
602    }
603}