Skip to main content

rainier_http/
request.rs

1//! The incoming [`Request`].
2//!
3//! Modelled on Laravel's `Illuminate\Http\Request`: one object carrying the
4//! HTTP message *and* the conveniences an application actually reaches for —
5//! `input()`, `file()`, `cookie()`, `bearer_token()`, `expects_json()`.
6//!
7//! Parsing is **lazy but synchronous**. The body arrives already buffered, and
8//! the query string, cookies and body are each parsed on first access and
9//! cached in a `OnceLock`. A request that only reads a header never runs the
10//! form parser; a request that reads `input()` twice runs it once.
11
12use std::collections::HashMap;
13use std::sync::OnceLock;
14
15use bytes::Bytes;
16use http::{HeaderMap, Method, Uri, Version};
17use rainier_support::{Error, Extensions, Result};
18use serde::de::DeserializeOwned;
19use serde_json::Value;
20
21use crate::cookie::parse_cookie_header;
22use crate::input;
23use crate::upload::{self, Multipart, UploadedFile};
24
25/// The client's address, put into the request's extensions by the server.
26///
27/// A newtype rather than a bare `SocketAddr` so it cannot collide with another
28/// address an application stores in the same type-keyed bag.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ClientIp(pub std::net::IpAddr);
31
32/// How a request body was interpreted.
33#[derive(Debug)]
34enum ParsedBody {
35    /// `application/json`, parsed.
36    Json(Value),
37    /// `application/x-www-form-urlencoded`, lifted into a JSON tree.
38    Form(Value),
39    /// `multipart/form-data`, split into fields and files.
40    Multipart(Box<Multipart>),
41    /// No body, an unrecognised content type, or a body that failed to parse.
42    /// Failures land here rather than erroring, because a handler that never
43    /// looks at the body should not care that it was malformed — the error
44    /// surfaces from [`Request::json`], which is where it can be reported.
45    Opaque,
46}
47
48/// An inbound HTTP request.
49#[derive(Debug)]
50pub struct Request {
51    method: Method,
52    uri: Uri,
53    version: Version,
54    headers: HeaderMap,
55    body: Bytes,
56    extensions: Extensions,
57    route_params: HashMap<String, String>,
58    /// Set by input-rewriting middleware (`TrimStrings`,
59    /// `ConvertEmptyStringsToNull`) and by `merge`. When present it *replaces*
60    /// the query-plus-body view entirely rather than layering over it — it was
61    /// derived from that view, so consulting the originals underneath it would
62    /// resurrect exactly the values the rewrite removed.
63    input_override: Option<Value>,
64
65    query: OnceLock<Value>,
66    parsed_body: OnceLock<ParsedBody>,
67    cookies: OnceLock<HashMap<String, String>>,
68}
69
70impl Request {
71    /// Build a request from its parts. The server calls this; tests should
72    /// prefer [`Request::builder`].
73    pub fn new(method: Method, uri: Uri, headers: HeaderMap, body: Bytes) -> Self {
74        Self {
75            method,
76            uri,
77            version: Version::HTTP_11,
78            headers,
79            body,
80            extensions: Extensions::new(),
81            route_params: HashMap::new(),
82            input_override: None,
83            query: OnceLock::new(),
84            parsed_body: OnceLock::new(),
85            cookies: OnceLock::new(),
86        }
87    }
88
89    /// A fluent builder, mainly for tests.
90    pub fn builder() -> RequestBuilder {
91        RequestBuilder::new()
92    }
93
94    /// Adopt an `http::Request` whose body has already been buffered.
95    pub fn from_http(request: http::Request<Bytes>) -> Self {
96        let (parts, body) = request.into_parts();
97        let mut request = Self::new(parts.method, parts.uri, parts.headers, body);
98        request.version = parts.version;
99        request
100    }
101
102    // --- the message ------------------------------------------------------
103
104    /// The HTTP method.
105    pub fn method(&self) -> &Method {
106        &self.method
107    }
108
109    /// The full request URI.
110    pub fn uri(&self) -> &Uri {
111        &self.uri
112    }
113
114    /// The path, without the query string. Always begins with `/`.
115    pub fn path(&self) -> &str {
116        self.uri.path()
117    }
118
119    /// The raw query string, without the `?`.
120    pub fn query_string(&self) -> &str {
121        self.uri.query().unwrap_or("")
122    }
123
124    /// The HTTP version.
125    pub fn version(&self) -> Version {
126        self.version
127    }
128
129    /// Every header.
130    pub fn headers(&self) -> &HeaderMap {
131        &self.headers
132    }
133
134    /// Mutable access to the headers — for middleware that normalises them.
135    pub fn headers_mut(&mut self) -> &mut HeaderMap {
136        &mut self.headers
137    }
138
139    /// One header as a string, or `None` if absent or not valid UTF-8.
140    pub fn header(&self, name: &str) -> Option<&str> {
141        self.headers.get(name)?.to_str().ok()
142    }
143
144    /// Replace the method.
145    ///
146    /// For the one middleware that legitimately needs it: turning a browser
147    /// form's `POST` with `_method=DELETE` into a real `DELETE` before routing
148    /// sees it. See
149    /// [`MethodOverride`](https://docs.rs/rainier-middleware) — and note that
150    /// it must run **before** the router, or the route has already been chosen
151    /// for the old method.
152    pub fn set_method(&mut self, method: Method) {
153        self.method = method;
154    }
155
156    /// Whether the method is `method`.
157    pub fn is_method(&self, method: &Method) -> bool {
158        self.method == method
159    }
160
161    /// The raw body bytes.
162    pub fn body(&self) -> &Bytes {
163        &self.body
164    }
165
166    /// The body as UTF-8, lossily.
167    pub fn body_string(&self) -> String {
168        String::from_utf8_lossy(&self.body).into_owned()
169    }
170
171    /// The `Content-Type` header, lowercased and without parameters.
172    pub fn content_type(&self) -> Option<String> {
173        let raw = self.header("content-type")?;
174        let base = raw.split(';').next().unwrap_or(raw);
175        Some(base.trim().to_ascii_lowercase())
176    }
177
178    /// Whether the body claims to be JSON. Also true for `+json` suffixes such
179    /// as `application/ld+json`.
180    pub fn is_json(&self) -> bool {
181        self.content_type().is_some_and(|ct| ct == "application/json" || ct.ends_with("+json"))
182    }
183
184    /// Whether the client would rather have JSON back.
185    ///
186    /// True when it sent JSON, when it asked for JSON in `Accept`, or when it
187    /// looks like an XHR. This is what the exception handler consults to decide
188    /// between a JSON error body and an HTML error page.
189    pub fn expects_json(&self) -> bool {
190        if self.is_json() {
191            return true;
192        }
193        if self.header("x-requested-with").is_some_and(|v| v.eq_ignore_ascii_case("XMLHttpRequest"))
194        {
195            return true;
196        }
197        match self.header("accept") {
198            // `*/*` is what a plain browser navigation and curl both send, so
199            // it must not count as "wants JSON".
200            Some(accept) => accept.split(',').any(|part| {
201                let part = part.split(';').next().unwrap_or(part).trim();
202                part == "application/json" || part.ends_with("+json")
203            }),
204            None => false,
205        }
206    }
207
208    // --- input ------------------------------------------------------------
209
210    /// The query string parsed into a JSON tree.
211    pub fn query(&self) -> &Value {
212        self.query.get_or_init(|| input::parse_urlencoded(self.query_string().as_bytes()))
213    }
214
215    fn parsed_body(&self) -> &ParsedBody {
216        self.parsed_body.get_or_init(|| {
217            if self.body.is_empty() {
218                return ParsedBody::Opaque;
219            }
220            match self.content_type().as_deref() {
221                Some(ct) if ct == "application/json" || ct.ends_with("+json") => {
222                    match serde_json::from_slice(&self.body) {
223                        Ok(value) => ParsedBody::Json(value),
224                        Err(_) => ParsedBody::Opaque,
225                    }
226                }
227                Some("application/x-www-form-urlencoded") => {
228                    ParsedBody::Form(input::parse_urlencoded(&self.body))
229                }
230                Some("multipart/form-data") => {
231                    let boundary = self.header("content-type").and_then(upload::boundary_of);
232                    match boundary.and_then(|b| upload::parse(&self.body, &b).ok()) {
233                        Some(parsed) => ParsedBody::Multipart(Box::new(parsed)),
234                        None => ParsedBody::Opaque,
235                    }
236                }
237                _ => ParsedBody::Opaque,
238            }
239        })
240    }
241
242    /// The body's fields as a JSON tree — `{}` when there is no parseable body.
243    pub fn body_input(&self) -> &Value {
244        static EMPTY: OnceLock<Value> = OnceLock::new();
245        if let Some(overridden) = &self.input_override {
246            return overridden;
247        }
248        match self.parsed_body() {
249            ParsedBody::Json(value) => value,
250            ParsedBody::Form(value) => value,
251            ParsedBody::Multipart(parsed) => &parsed.fields,
252            ParsedBody::Opaque => EMPTY.get_or_init(|| Value::Object(serde_json::Map::new())),
253        }
254    }
255
256    /// Every input: the query string with the body merged over it.
257    ///
258    /// Route parameters are **not** included, matching Laravel — they are part
259    /// of the URL's shape rather than user input, and mixing them in would let
260    /// a query string shadow a route binding. Read them with
261    /// [`route_param`](Self::route_param).
262    pub fn all(&self) -> Value {
263        match &self.input_override {
264            Some(overridden) => overridden.clone(),
265            None => input::merge(self.query().clone(), self.body_input().clone()),
266        }
267    }
268
269    /// One input by dotted key, as a string. Body wins over query string.
270    pub fn input(&self, key: &str) -> Option<String> {
271        if let Some(overridden) = &self.input_override {
272            return input::lookup(overridden, key).and_then(input::scalar_to_string);
273        }
274        input::lookup(self.body_input(), key)
275            .or_else(|| input::lookup(self.query(), key))
276            .and_then(input::scalar_to_string)
277    }
278
279    /// Rewrite every input through `transform`.
280    ///
281    /// The seam input-normalising middleware works through: `TrimStrings` and
282    /// `ConvertEmptyStringsToNull` both take [`all`](Self::all), reshape it,
283    /// and hand it back. Later reads see only the rewritten values.
284    pub fn transform_input(&mut self, transform: impl FnOnce(Value) -> Value) {
285        self.input_override = Some(transform(self.all()));
286    }
287
288    /// Replace every input outright.
289    pub fn set_input(&mut self, value: Value) {
290        self.input_override = Some(value);
291    }
292
293    /// Merge extra values over the current inputs — Laravel's
294    /// `$request->merge()`, for a middleware that resolves something the
295    /// handler should read as ordinary input.
296    pub fn merge_input(&mut self, extra: Value) {
297        self.transform_input(|current| input::merge(current, extra));
298    }
299
300    /// One input, or `default`.
301    pub fn input_or(&self, key: &str, default: impl Into<String>) -> String {
302        self.input(key).unwrap_or_else(|| default.into())
303    }
304
305    /// The raw JSON value at `key`, for non-scalar inputs.
306    pub fn input_value(&self, key: &str) -> Option<&Value> {
307        if let Some(overridden) = &self.input_override {
308            return input::lookup(overridden, key);
309        }
310        input::lookup(self.body_input(), key).or_else(|| input::lookup(self.query(), key))
311    }
312
313    /// Whether `key` is present at all (even if empty).
314    pub fn has(&self, key: &str) -> bool {
315        self.input_value(key).is_some()
316    }
317
318    /// Whether input-rewriting middleware has replaced the inputs.
319    pub fn input_was_rewritten(&self) -> bool {
320        self.input_override.is_some()
321    }
322
323    /// Whether `key` is present and not empty.
324    pub fn filled(&self, key: &str) -> bool {
325        match self.input_value(key) {
326            Some(Value::String(s)) => !s.trim().is_empty(),
327            Some(Value::Null) | None => false,
328            Some(Value::Array(items)) => !items.is_empty(),
329            Some(Value::Object(map)) => !map.is_empty(),
330            Some(_) => true,
331        }
332    }
333
334    /// Deserialise the JSON body into `T`.
335    ///
336    /// Fails with a 400 when the body is absent, is not JSON, or does not fit
337    /// `T` — unlike [`input`](Self::input), which shrugs at a bad body.
338    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
339        if self.body.is_empty() {
340            return Err(Error::bad_request("a JSON body is required but the request had none"));
341        }
342        if !self.is_json() {
343            return Err(Error::bad_request(format!(
344                "expected a JSON body but the Content-Type was `{}`",
345                self.content_type().unwrap_or_else(|| "(none)".into())
346            )));
347        }
348        serde_json::from_slice(&self.body)
349            .map_err(|e| Error::bad_request(format!("malformed JSON body: {e}")))
350    }
351
352    /// Deserialise the merged inputs into `T`.
353    ///
354    /// Every value from a form or query string is a string, so `T` should
355    /// either use string fields or `#[serde(deserialize_with = ..)]` coercions.
356    /// For typed coercion from forms, validate instead.
357    pub fn form<T: DeserializeOwned>(&self) -> Result<T> {
358        serde_json::from_value(self.all())
359            .map_err(|e| Error::bad_request(format!("could not read the request input: {e}")))
360    }
361
362    /// Deserialise the query string into `T`.
363    pub fn query_as<T: DeserializeOwned>(&self) -> Result<T> {
364        serde_json::from_value(self.query().clone())
365            .map_err(|e| Error::bad_request(format!("could not read the query string: {e}")))
366    }
367
368    // --- files -------------------------------------------------------------
369
370    /// Every file uploaded under `field`.
371    pub fn files(&self, field: &str) -> &[UploadedFile] {
372        match self.parsed_body() {
373            ParsedBody::Multipart(parsed) => {
374                parsed.files.get(field).map(Vec::as_slice).unwrap_or(&[])
375            }
376            _ => &[],
377        }
378    }
379
380    /// The first file uploaded under `field`.
381    pub fn file(&self, field: &str) -> Option<&UploadedFile> {
382        self.files(field).first()
383    }
384
385    /// Whether a non-empty file arrived under `field`.
386    pub fn has_file(&self, field: &str) -> bool {
387        self.file(field).is_some_and(|f| !f.is_empty())
388    }
389
390    // --- cookies -----------------------------------------------------------
391
392    /// Every cookie the client sent.
393    pub fn cookies(&self) -> &HashMap<String, String> {
394        self.cookies.get_or_init(|| match self.header("cookie") {
395            Some(header) => parse_cookie_header(header),
396            None => HashMap::new(),
397        })
398    }
399
400    /// One cookie's value.
401    pub fn cookie(&self, name: &str) -> Option<&str> {
402        self.cookies().get(name).map(String::as_str)
403    }
404
405    /// The `Authorization: Bearer …` token, if present.
406    pub fn bearer_token(&self) -> Option<&str> {
407        let header = self.header("authorization")?;
408        let (scheme, token) = header.split_once(' ')?;
409        scheme.eq_ignore_ascii_case("bearer").then(|| token.trim())
410    }
411
412    // --- route parameters --------------------------------------------------
413
414    /// A parameter captured by the matched route (`/posts/{post}`).
415    pub fn route_param(&self, name: &str) -> Option<&str> {
416        self.route_params.get(name).map(String::as_str)
417    }
418
419    /// Every captured route parameter.
420    pub fn route_params(&self) -> &HashMap<String, String> {
421        &self.route_params
422    }
423
424    /// Replace the captured route parameters. The router calls this on match.
425    pub fn set_route_params(&mut self, params: HashMap<String, String>) {
426        self.route_params = params;
427    }
428
429    // --- extensions --------------------------------------------------------
430
431    /// Per-request attributes, keyed by type: the authenticated user, the
432    /// matched route, a resolved model binding.
433    pub fn extensions(&self) -> &Extensions {
434        &self.extensions
435    }
436
437    /// Mutable per-request attributes.
438    pub fn extensions_mut(&mut self) -> &mut Extensions {
439        &mut self.extensions
440    }
441
442    /// Store a per-request attribute, returning `self` for chaining.
443    pub fn with_extension<T: Send + Sync + 'static>(mut self, value: T) -> Self {
444        self.extensions.insert(value);
445        self
446    }
447
448    /// Read a per-request attribute.
449    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
450        self.extensions.get::<T>()
451    }
452
453    /// The client's IP, if the server recorded one.
454    pub fn ip(&self) -> Option<std::net::IpAddr> {
455        self.extensions.get::<ClientIp>().map(|ip| ip.0)
456    }
457}
458
459/// A fluent [`Request`] builder.
460///
461/// ```
462/// # use rainier_http::Request;
463/// # use http::Method;
464/// let request = Request::builder()
465///     .method(Method::POST)
466///     .uri("/posts?draft=1")
467///     .json(&serde_json::json!({ "title": "Hello" }))
468///     .build();
469///
470/// assert_eq!(request.input("title").as_deref(), Some("Hello"));
471/// assert_eq!(request.input("draft").as_deref(), Some("1"));
472/// ```
473#[derive(Debug)]
474pub struct RequestBuilder {
475    method: Method,
476    uri: String,
477    headers: HeaderMap,
478    body: Bytes,
479    route_params: HashMap<String, String>,
480}
481
482impl Default for RequestBuilder {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488impl RequestBuilder {
489    /// A `GET /` request with no headers or body.
490    pub fn new() -> Self {
491        Self {
492            method: Method::GET,
493            uri: "/".to_string(),
494            headers: HeaderMap::new(),
495            body: Bytes::new(),
496            route_params: HashMap::new(),
497        }
498    }
499
500    /// Set the method.
501    pub fn method(mut self, method: Method) -> Self {
502        self.method = method;
503        self
504    }
505
506    /// Set the URI (path and optional query string).
507    pub fn uri(mut self, uri: impl Into<String>) -> Self {
508        self.uri = uri.into();
509        self
510    }
511
512    /// Add a header. Ignored if the name or value is not a legal header.
513    pub fn header(mut self, name: &str, value: &str) -> Self {
514        if let (Ok(name), Ok(value)) =
515            (http::header::HeaderName::try_from(name), http::header::HeaderValue::from_str(value))
516        {
517            self.headers.insert(name, value);
518        }
519        self
520    }
521
522    /// Set a raw body.
523    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
524        self.body = body.into();
525        self
526    }
527
528    /// Set a JSON body and the matching `Content-Type`.
529    pub fn json(self, value: &impl serde::Serialize) -> Self {
530        let encoded = serde_json::to_vec(value).unwrap_or_default();
531        self.header("content-type", "application/json").body(encoded)
532    }
533
534    /// Set a urlencoded form body and the matching `Content-Type`.
535    pub fn form(self, pairs: &[(&str, &str)]) -> Self {
536        let encoded: String =
537            form_urlencoded::Serializer::new(String::new()).extend_pairs(pairs).finish();
538        self.header("content-type", "application/x-www-form-urlencoded").body(encoded)
539    }
540
541    /// Pre-set a route parameter, as the router would after matching.
542    pub fn route_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
543        self.route_params.insert(name.into(), value.into());
544        self
545    }
546
547    /// Build the request.
548    pub fn build(self) -> Request {
549        let uri: Uri = self.uri.parse().unwrap_or_else(|_| Uri::from_static("/"));
550        let mut request = Request::new(self.method, uri, self.headers, self.body);
551        request.route_params = self.route_params;
552        request
553    }
554}
555
556#[cfg(test)]
557mod input_rewriting_tests {
558    use super::*;
559    use serde_json::json;
560
561    #[test]
562    fn transform_input_replaces_what_later_reads_see() {
563        let mut request = Request::builder().uri("/x?name=+ada+&age=36").build();
564        assert_eq!(request.input("name").as_deref(), Some(" ada "));
565
566        request.transform_input(|value| {
567            let mut object = value.as_object().cloned().unwrap_or_default();
568            if let Some(Value::String(name)) = object.get_mut("name") {
569                *name = name.trim().to_string();
570            }
571            Value::Object(object)
572        });
573
574        assert!(request.input_was_rewritten());
575        assert_eq!(request.input("name").as_deref(), Some("ada"));
576        assert_eq!(request.all(), json!({ "name": "ada", "age": "36" }));
577    }
578
579    #[test]
580    fn a_rewrite_is_not_undone_by_the_original_query_string() {
581        // Regression guard: if `input()` fell back to the query string after a
582        // rewrite, removing a value would be impossible.
583        let mut request = Request::builder().uri("/x?secret=leak").build();
584        request.set_input(json!({}));
585        assert_eq!(request.input("secret"), None);
586        assert!(!request.has("secret"));
587    }
588
589    #[test]
590    fn merge_input_layers_over_the_current_values() {
591        let mut request = Request::builder().uri("/x?a=1").build();
592        request.merge_input(json!({ "b": "2" }));
593        assert_eq!(request.all(), json!({ "a": "1", "b": "2" }));
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use serde_json::json;
601
602    #[test]
603    fn exposes_the_message_basics() {
604        let request = Request::builder().method(Method::POST).uri("/posts?page=2").build();
605        assert_eq!(request.method(), Method::POST);
606        assert_eq!(request.path(), "/posts");
607        assert_eq!(request.query_string(), "page=2");
608        assert!(request.is_method(&Method::POST));
609    }
610
611    #[test]
612    fn reads_query_string_input() {
613        let request = Request::builder().uri("/search?q=rust&tags[]=a&tags[]=b").build();
614        assert_eq!(request.input("q").as_deref(), Some("rust"));
615        assert_eq!(request.input_value("tags"), Some(&json!(["a", "b"])));
616    }
617
618    #[test]
619    fn reads_json_body_input() {
620        let request = Request::builder()
621            .method(Method::POST)
622            .json(&json!({ "title": "Hi", "meta": { "tag": "rust" } }))
623            .build();
624
625        assert_eq!(request.input("title").as_deref(), Some("Hi"));
626        assert_eq!(request.input("meta.tag").as_deref(), Some("rust"));
627        assert!(request.is_json());
628    }
629
630    #[test]
631    fn reads_form_body_input() {
632        let request = Request::builder()
633            .method(Method::POST)
634            .form(&[("title", "Hi"), ("body", "there")])
635            .build();
636        assert_eq!(request.input("title").as_deref(), Some("Hi"));
637        assert_eq!(request.input("body").as_deref(), Some("there"));
638    }
639
640    #[test]
641    fn the_body_wins_over_the_query_string() {
642        let request = Request::builder()
643            .method(Method::POST)
644            .uri("/x?name=query")
645            .json(&json!({ "name": "body" }))
646            .build();
647        assert_eq!(request.input("name").as_deref(), Some("body"));
648        assert_eq!(request.all(), json!({ "name": "body" }));
649    }
650
651    #[test]
652    fn all_merges_query_and_body() {
653        let request = Request::builder()
654            .method(Method::POST)
655            .uri("/x?page=2")
656            .json(&json!({ "name": "ada" }))
657            .build();
658        assert_eq!(request.all(), json!({ "page": "2", "name": "ada" }));
659    }
660
661    #[test]
662    fn route_params_stay_out_of_input() {
663        let request = Request::builder().uri("/posts/7").route_param("post", "7").build();
664        assert_eq!(request.route_param("post"), Some("7"));
665        assert!(!request.has("post"), "route params must not shadow or appear as input");
666    }
667
668    #[test]
669    fn has_and_filled_differ_on_blank_values() {
670        let request = Request::builder().uri("/x?a=&b=v&c[]=").build();
671        assert!(request.has("a"));
672        assert!(!request.filled("a"));
673        assert!(request.filled("b"));
674        assert!(!request.has("missing"));
675    }
676
677    #[test]
678    fn json_reports_why_it_failed() {
679        let empty = Request::builder().build();
680        assert!(empty.json::<Value>().unwrap_err().message().contains("had none"));
681
682        let wrong_type = Request::builder().body("hello").build();
683        assert!(wrong_type.json::<Value>().unwrap_err().message().contains("Content-Type"));
684
685        let malformed =
686            Request::builder().header("content-type", "application/json").body("{oops").build();
687        let err = malformed.json::<Value>().unwrap_err();
688        assert_eq!(err.status(), 400);
689        assert!(err.message().contains("malformed JSON"));
690    }
691
692    #[test]
693    fn a_malformed_body_does_not_break_unrelated_accessors() {
694        let request = Request::builder()
695            .header("content-type", "application/json")
696            .uri("/x?page=2")
697            .body("{oops")
698            .build();
699        // The handler never looks at the body; reading a query param must work.
700        assert_eq!(request.input("page").as_deref(), Some("2"));
701        assert_eq!(request.body_input(), &json!({}));
702    }
703
704    #[test]
705    fn detects_json_content_types_including_suffixes() {
706        let plain =
707            Request::builder().header("content-type", "application/json; charset=utf-8").build();
708        assert!(plain.is_json());
709        assert_eq!(plain.content_type().as_deref(), Some("application/json"));
710
711        let suffixed = Request::builder().header("content-type", "application/ld+json").build();
712        assert!(suffixed.is_json());
713
714        let html = Request::builder().header("content-type", "text/html").build();
715        assert!(!html.is_json());
716    }
717
718    #[test]
719    fn expects_json_reads_the_accept_header() {
720        let asked = Request::builder().header("accept", "application/json").build();
721        assert!(asked.expects_json());
722
723        let xhr = Request::builder().header("x-requested-with", "XMLHttpRequest").build();
724        assert!(xhr.expects_json());
725
726        let browser = Request::builder()
727            .header("accept", "text/html,application/xhtml+xml,*/*;q=0.8")
728            .build();
729        assert!(!browser.expects_json(), "`*/*` must not count as wanting JSON");
730
731        assert!(!Request::builder().build().expects_json());
732    }
733
734    #[test]
735    fn reads_cookies() {
736        let request = Request::builder().header("cookie", "session=abc; theme=dark").build();
737        assert_eq!(request.cookie("session"), Some("abc"));
738        assert_eq!(request.cookie("nope"), None);
739        assert_eq!(request.cookies().len(), 2);
740    }
741
742    #[test]
743    fn reads_a_bearer_token() {
744        let request = Request::builder().header("authorization", "Bearer tok_123").build();
745        assert_eq!(request.bearer_token(), Some("tok_123"));
746
747        // Case-insensitive scheme, per RFC 7235.
748        let lower = Request::builder().header("authorization", "bearer tok_123").build();
749        assert_eq!(lower.bearer_token(), Some("tok_123"));
750
751        let basic = Request::builder().header("authorization", "Basic abc").build();
752        assert_eq!(basic.bearer_token(), None);
753        assert_eq!(Request::builder().build().bearer_token(), None);
754    }
755
756    #[test]
757    fn extensions_carry_typed_attributes() {
758        #[derive(Debug, PartialEq)]
759        struct CurrentUser(u64);
760
761        let request = Request::builder().build().with_extension(CurrentUser(7));
762        assert_eq!(request.extension::<CurrentUser>(), Some(&CurrentUser(7)));
763        assert_eq!(request.extension::<ClientIp>(), None);
764    }
765
766    #[test]
767    fn deserialises_the_json_body_into_a_struct() {
768        #[derive(serde::Deserialize, PartialEq, Debug)]
769        struct NewPost {
770            title: String,
771            draft: bool,
772        }
773
774        let request = Request::builder()
775            .method(Method::POST)
776            .json(&json!({ "title": "Hi", "draft": true }))
777            .build();
778        assert_eq!(request.json::<NewPost>().unwrap(), NewPost { title: "Hi".into(), draft: true });
779    }
780
781    #[test]
782    fn parses_uploads_from_a_multipart_body() {
783        let boundary = "X";
784        let body = format!(
785            "--{boundary}\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nHi\r\n\
786             --{boundary}\r\nContent-Disposition: form-data; name=\"doc\"; filename=\"a.txt\"\r\n\r\nDATA\r\n\
787             --{boundary}--\r\n"
788        );
789        let request = Request::builder()
790            .method(Method::POST)
791            .header("content-type", &format!("multipart/form-data; boundary={boundary}"))
792            .body(body)
793            .build();
794
795        assert_eq!(request.input("title").as_deref(), Some("Hi"));
796        assert!(request.has_file("doc"));
797        assert_eq!(request.file("doc").unwrap().bytes().as_ref(), b"DATA");
798        assert!(!request.has_file("missing"));
799        assert!(request.files("missing").is_empty());
800    }
801
802    #[test]
803    fn parsing_is_cached_across_calls() {
804        let request = Request::builder().uri("/x?a=1").build();
805        let first = request.query() as *const Value;
806        let second = request.query() as *const Value;
807        assert_eq!(first, second, "the query string must be parsed once");
808    }
809
810    #[test]
811    fn adopts_an_http_request() {
812        let inner = http::Request::builder()
813            .method(Method::PUT)
814            .uri("/things/1")
815            .header("x-test", "yes")
816            .body(Bytes::from("payload"))
817            .unwrap();
818
819        let request = Request::from_http(inner);
820        assert_eq!(request.method(), Method::PUT);
821        assert_eq!(request.path(), "/things/1");
822        assert_eq!(request.header("x-test"), Some("yes"));
823        assert_eq!(request.body_string(), "payload");
824    }
825}