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}