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