Skip to main content

ferro_rs/http/
request.rs

1use super::body::{collect_body, parse_form, parse_json};
2use super::cookie::parse_cookies;
3use super::ParamError;
4use crate::error::FrameworkError;
5use bytes::Bytes;
6use serde::de::DeserializeOwned;
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9
10/// HTTP Request wrapper providing Laravel-like access to request data
11pub struct Request {
12    inner: hyper::Request<hyper::body::Incoming>,
13    params: HashMap<String, String>,
14    extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
15    /// Route pattern for metrics (e.g., "/users/{id}" instead of "/users/123")
16    route_pattern: Option<String>,
17}
18
19impl Request {
20    /// Create a new request from a raw hyper request.
21    pub fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
22        Self {
23            inner,
24            params: HashMap::new(),
25            extensions: HashMap::new(),
26            route_pattern: None,
27        }
28    }
29
30    /// Attach route parameters extracted from the URL path.
31    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
32        self.params = params;
33        self
34    }
35
36    /// Set the route pattern (e.g., "/users/{id}")
37    pub fn with_route_pattern(mut self, pattern: String) -> Self {
38        self.route_pattern = Some(pattern);
39        self
40    }
41
42    /// Get the route pattern for metrics grouping
43    pub fn route_pattern(&self) -> Option<String> {
44        self.route_pattern.clone()
45    }
46
47    /// Insert a value into the request extensions (type-map pattern)
48    ///
49    /// This is async-safe unlike thread-local storage.
50    pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
51        self.extensions.insert(TypeId::of::<T>(), Box::new(value));
52    }
53
54    /// Get a reference to a value from the request extensions
55    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
56        self.extensions
57            .get(&TypeId::of::<T>())
58            .and_then(|boxed| boxed.downcast_ref::<T>())
59    }
60
61    /// Get a mutable reference to a value from the request extensions
62    pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
63        self.extensions
64            .get_mut(&TypeId::of::<T>())
65            .and_then(|boxed| boxed.downcast_mut::<T>())
66    }
67
68    /// Get the request method
69    pub fn method(&self) -> &hyper::Method {
70        self.inner.method()
71    }
72
73    /// Get the request path
74    pub fn path(&self) -> &str {
75        self.inner.uri().path()
76    }
77
78    /// Rewrite the request path (server-side only — the browser URL is unchanged).
79    ///
80    /// Replaces the URI path component while preserving the scheme, authority, and
81    /// query string. Used by pre-route middleware (e.g. `HostMiddleware`) to map
82    /// custom-domain requests onto internal slug-based routes before routing occurs.
83    ///
84    /// `new_path` must begin with `/`. Panics in debug mode if it does not.
85    pub fn set_path(&mut self, new_path: &str) {
86        debug_assert!(
87            new_path.starts_with('/'),
88            "set_path: path must begin with '/', got {new_path:?}"
89        );
90        let old_uri = self.inner.uri();
91        // Preserve scheme, authority, and query string; replace path only.
92        let mut parts = old_uri.clone().into_parts();
93        let path_and_query = match old_uri.query() {
94            Some(q) => format!("{new_path}?{q}"),
95            None => new_path.to_string(),
96        };
97        parts.path_and_query = Some(
98            path_and_query
99                .parse()
100                .unwrap_or_else(|_| new_path.parse().expect("invalid path")),
101        );
102        if let Ok(new_uri) = http::Uri::from_parts(parts) {
103            *self.inner.uri_mut() = new_uri;
104        }
105    }
106
107    /// Get a route parameter by name (e.g., /users/{id})
108    /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
109    pub fn param(&self, name: &str) -> Result<&str, ParamError> {
110        self.params
111            .get(name)
112            .map(|s| s.as_str())
113            .ok_or_else(|| ParamError {
114                param_name: name.to_string(),
115            })
116    }
117
118    /// Get a route parameter parsed as a specific type
119    ///
120    /// Combines `param()` with parsing, returning a typed value.
121    ///
122    /// # Example
123    ///
124    /// ```rust,ignore
125    /// pub async fn show(req: Request) -> Response {
126    ///     let id: i32 = req.param_as("id")?;
127    ///     // ...
128    /// }
129    /// ```
130    pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Result<T, ParamError>
131    where
132        T::Err: std::fmt::Display,
133    {
134        let value = self.param(name)?;
135        value.parse::<T>().map_err(|e| ParamError {
136            param_name: format!("{name} (parse error: {e})"),
137        })
138    }
139
140    /// Get all route parameters
141    pub fn params(&self) -> &HashMap<String, String> {
142        &self.params
143    }
144
145    /// Get a query string parameter by name
146    ///
147    /// # Example
148    ///
149    /// ```rust,ignore
150    /// // URL: /users?page=2&limit=10
151    /// let page = req.query("page"); // Some("2")
152    /// let sort = req.query("sort"); // None
153    /// ```
154    pub fn query(&self, name: &str) -> Option<String> {
155        self.inner.uri().query().and_then(|q| {
156            form_urlencoded::parse(q.as_bytes())
157                .find(|(key, _)| key == name)
158                .map(|(_, value)| value.into_owned())
159        })
160    }
161
162    /// Get a query string parameter or a default value
163    ///
164    /// # Example
165    ///
166    /// ```rust,ignore
167    /// // URL: /users?page=2
168    /// let page = req.query_or("page", "1"); // "2"
169    /// let limit = req.query_or("limit", "10"); // "10"
170    /// ```
171    pub fn query_or(&self, name: &str, default: &str) -> String {
172        self.query(name).unwrap_or_else(|| default.to_string())
173    }
174
175    /// Get a query string parameter parsed as a specific type
176    ///
177    /// # Example
178    ///
179    /// ```rust,ignore
180    /// // URL: /users?page=2&limit=10
181    /// let page: Option<i32> = req.query_as("page"); // Some(2)
182    /// ```
183    pub fn query_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
184        self.query(name).and_then(|v| v.parse().ok())
185    }
186
187    /// Get a query string parameter parsed as a specific type, or a default
188    ///
189    /// # Example
190    ///
191    /// ```rust,ignore
192    /// // URL: /users?page=2
193    /// let page: i32 = req.query_as_or("page", 1); // 2
194    /// let limit: i32 = req.query_as_or("limit", 10); // 10
195    /// ```
196    pub fn query_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
197        self.query_as(name).unwrap_or(default)
198    }
199
200    // ── Phase 137: validation flash round-trip helpers ────────────────────────
201
202    /// Read a previously-submitted form value from session flash.
203    ///
204    /// After a POST handler calls `ValidationError::with_old_input(&data).redirect_back(...)`,
205    /// the GET handler retrieves the value with `req.old("field_name")` and passes it as
206    /// `InputProps.default_value` to repopulate the form.
207    ///
208    /// Reads from `_flash.old._old_input.<field>` without clearing (read-only semantics).
209    /// Flash aging (move new→old→deleted) is handled by the session middleware at request
210    /// boundaries, so multiple reads in the same GET handler are safe.
211    ///
212    /// Returns `None` when no flash value exists, no session is active, or the key is absent.
213    pub fn old(&self, field: &str) -> Option<String> {
214        let key = format!("_flash.old._old_input.{field}");
215        crate::session::session().and_then(|s| s.get::<String>(&key))
216    }
217
218    /// Read the first validation error message for a field from session flash.
219    ///
220    /// After a POST handler calls `errors.redirect_back(...)`, the GET handler calls
221    /// `req.validation_error("field_name")` and passes the result as `InputProps.error`.
222    ///
223    /// Reads from `_flash.old._validation_errors` without clearing (read-only semantics).
224    ///
225    /// Returns `None` when no flash errors exist, no session is active, or the field has no error.
226    pub fn validation_error(&self, field: &str) -> Option<String> {
227        let errors: Option<std::collections::HashMap<String, Vec<String>>> =
228            crate::session::session().and_then(|s| {
229                s.get::<std::collections::HashMap<String, Vec<String>>>(
230                    "_flash.old._validation_errors",
231                )
232            });
233        errors.and_then(|map| map.get(field).and_then(|v| v.first()).cloned())
234    }
235
236    /// Returns `true` when any validation errors were flashed from a prior request.
237    ///
238    /// Useful for rendering a form-wide error summary banner.
239    pub fn has_validation_errors(&self) -> bool {
240        crate::session::session()
241            .and_then(|s| {
242                s.get::<std::collections::HashMap<String, Vec<String>>>(
243                    "_flash.old._validation_errors",
244                )
245            })
246            .map(|m| !m.is_empty())
247            .unwrap_or(false)
248    }
249
250    /// Get the inner hyper request
251    pub fn inner(&self) -> &hyper::Request<hyper::body::Incoming> {
252        &self.inner
253    }
254
255    /// Get a header value by name
256    pub fn header(&self, name: &str) -> Option<&str> {
257        self.inner.headers().get(name).and_then(|v| v.to_str().ok())
258    }
259
260    /// Get the Content-Type header
261    pub fn content_type(&self) -> Option<&str> {
262        self.header("content-type")
263    }
264
265    /// Resolve the URL the current request was triggered from, falling
266    /// back to `fallback` when the `Referer` is absent, malformed, or
267    /// points off-origin.
268    ///
269    /// Use to capture a "back" target at handler entry before the request
270    /// body is consumed (e.g. before [`body_bytes`](Self::body_bytes)). The
271    /// returned `String` then feeds [`crate::http::Redirect::to`] to send
272    /// the user back to where they came from, preserving query strings
273    /// (e.g. `?tab=note`) and any other URL state.
274    ///
275    /// Same-origin rule mirrors [`crate::http::Redirect::back`]: absolute
276    /// URLs must share the request's `Host`; scheme-relative URLs are
277    /// rejected.
278    pub fn back_or(&self, fallback: impl Into<String>) -> String {
279        let referer = match self.header("referer") {
280            Some(r) => r,
281            None => return fallback.into(),
282        };
283        if referer.starts_with("//") {
284            return fallback.into();
285        }
286        if referer.starts_with('/') {
287            return referer.to_string();
288        }
289        let rest = match referer
290            .strip_prefix("http://")
291            .or_else(|| referer.strip_prefix("https://"))
292        {
293            Some(r) => r,
294            None => return fallback.into(),
295        };
296        let (referer_host, path) = match rest.find('/') {
297            Some(i) => (&rest[..i], &rest[i..]),
298            None => (rest, "/"),
299        };
300        let request_host = match self.header("host") {
301            Some(h) => h,
302            None => return fallback.into(),
303        };
304        if referer_host == request_host {
305            path.to_string()
306        } else {
307            fallback.into()
308        }
309    }
310
311    /// Check if this is an Inertia XHR request
312    pub fn is_inertia(&self) -> bool {
313        self.header("X-Inertia")
314            .map(|v| v == "true")
315            .unwrap_or(false)
316    }
317
318    /// Get all cookies from the request
319    ///
320    /// Parses the Cookie header and returns a HashMap of cookie names to values.
321    ///
322    /// # Example
323    ///
324    /// ```rust,ignore
325    /// let cookies = req.cookies();
326    /// if let Some(session) = cookies.get("session") {
327    ///     println!("Session: {}", session);
328    /// }
329    /// ```
330    pub fn cookies(&self) -> HashMap<String, String> {
331        self.header("Cookie").map(parse_cookies).unwrap_or_default()
332    }
333
334    /// Get a specific cookie value by name
335    ///
336    /// # Example
337    ///
338    /// ```rust,ignore
339    /// if let Some(session_id) = req.cookie("session") {
340    ///     // Use session_id
341    /// }
342    /// ```
343    pub fn cookie(&self, name: &str) -> Option<String> {
344        self.cookies().get(name).cloned()
345    }
346
347    /// Get the Inertia version from request headers
348    pub fn inertia_version(&self) -> Option<&str> {
349        self.header("X-Inertia-Version")
350    }
351
352    /// Get partial component name for partial reloads
353    pub fn inertia_partial_component(&self) -> Option<&str> {
354        self.header("X-Inertia-Partial-Component")
355    }
356
357    /// Get partial data keys for partial reloads
358    pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
359        self.header("X-Inertia-Partial-Data")
360            .map(|v| v.split(',').collect())
361    }
362
363    /// Consume the request and collect the body as bytes
364    pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
365        let content_type = self
366            .inner
367            .headers()
368            .get("content-type")
369            .and_then(|v| v.to_str().ok())
370            .map(|s| s.to_string());
371
372        let params = self.params;
373        let bytes = collect_body(self.inner.into_body()).await?;
374
375        Ok((
376            RequestParts {
377                params,
378                content_type,
379            },
380            bytes,
381        ))
382    }
383
384    /// Parse the request body as JSON
385    ///
386    /// Consumes the request since the body can only be read once.
387    ///
388    /// # Example
389    ///
390    /// ```rust,ignore
391    /// #[derive(Deserialize)]
392    /// struct CreateUser { name: String, email: String }
393    ///
394    /// pub async fn store(req: Request) -> Response {
395    ///     let data: CreateUser = req.json().await?;
396    ///     // ...
397    /// }
398    /// ```
399    pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
400        let (_, bytes) = self.body_bytes().await?;
401        parse_json(&bytes)
402    }
403
404    /// Parse the request body as form-urlencoded
405    ///
406    /// Consumes the request since the body can only be read once.
407    ///
408    /// # Example
409    ///
410    /// ```rust,ignore
411    /// #[derive(Deserialize)]
412    /// struct LoginForm { username: String, password: String }
413    ///
414    /// pub async fn login(req: Request) -> Response {
415    ///     let form: LoginForm = req.form().await?;
416    ///     // ...
417    /// }
418    /// ```
419    pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
420        let (_, bytes) = self.body_bytes().await?;
421        parse_form(&bytes)
422    }
423
424    /// Parse the request body as `multipart/form-data`.
425    ///
426    /// Consumes the request since the body can only be read once.
427    /// The per-field byte cap is read from `UPLOAD_MAX_SIZE_MB` (default 10 MiB),
428    /// and the per-request field cap from `UPLOAD_MAX_FIELDS` (default 100).
429    ///
430    /// # Errors
431    ///
432    /// Returns `FrameworkError::internal(...)` with the literal message
433    /// `"Content-Type is not multipart/form-data or missing boundary"` when
434    /// the request's `Content-Type` header is absent, malformed, or not a
435    /// multipart value.
436    ///
437    /// # Example
438    ///
439    /// ```rust,ignore
440    /// pub async fn upload(req: Request) -> Response {
441    ///     let form = req.multipart().await?;
442    ///     let title = form.field("title").unwrap_or_default();
443    ///     let file = form.file("attachment");
444    ///     // ...
445    /// }
446    /// ```
447    pub async fn multipart(self) -> Result<super::multipart::MultipartForm, FrameworkError> {
448        let content_type = self
449            .inner
450            .headers()
451            .get("content-type")
452            .and_then(|v| v.to_str().ok())
453            .map(|s| s.to_string())
454            .unwrap_or_default();
455        let body = self.inner.into_body();
456        super::multipart::parse_multipart_body(
457            body,
458            &content_type,
459            super::multipart::max_file_bytes(),
460            super::multipart::max_fields(),
461        )
462        .await
463    }
464
465    /// Parse the body as multipart/form-data and return the first file
466    /// uploaded under `field`.
467    ///
468    /// Consumes the request since the body can only be read once. Returns
469    /// `Ok(None)` when the multipart body parses successfully but contains
470    /// no file with that field name.
471    ///
472    /// # Example
473    ///
474    /// ```rust,ignore
475    /// pub async fn upload_avatar(req: Request) -> Response {
476    ///     let file = req.file("avatar").await?
477    ///         .ok_or_else(|| FrameworkError::internal("missing avatar"))?;
478    ///     // file.store(&disk, &path).await?;
479    ///     Ok(json!({"size": file.size()}))
480    /// }
481    /// ```
482    pub async fn file(
483        self,
484        field: &str,
485    ) -> Result<Option<super::multipart::UploadedFile>, FrameworkError> {
486        let mut form = self.multipart().await?;
487        Ok(form.files_map.remove(field).and_then(|mut v| {
488            if v.is_empty() {
489                None
490            } else {
491                Some(v.swap_remove(0))
492            }
493        }))
494    }
495
496    /// Parse the request body based on Content-Type header
497    ///
498    /// - `application/json` -> JSON parsing
499    /// - `application/x-www-form-urlencoded` -> Form parsing
500    /// - Otherwise -> JSON parsing (default)
501    ///
502    /// Consumes the request since the body can only be read once.
503    pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
504        let (parts, bytes) = self.body_bytes().await?;
505
506        match parts.content_type.as_deref() {
507            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
508            _ => parse_json(&bytes),
509        }
510    }
511
512    /// Consume the request and return its parts along with the inner hyper request body
513    ///
514    /// This is used internally by the handler macro for FormRequest extraction.
515    pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
516        let content_type = self
517            .inner
518            .headers()
519            .get("content-type")
520            .and_then(|v| v.to_str().ok())
521            .map(|s| s.to_string());
522
523        let params = self.params;
524        let body = self.inner.into_body();
525
526        (
527            RequestParts {
528                params,
529                content_type,
530            },
531            body,
532        )
533    }
534}
535
536/// Request parts after body has been separated
537///
538/// Contains metadata needed for body parsing without the body itself.
539#[derive(Clone)]
540pub struct RequestParts {
541    /// Route parameters extracted from the URL path.
542    pub params: HashMap<String, String>,
543    /// Content-Type header value, if present.
544    pub content_type: Option<String>,
545}
546
547#[cfg(test)]
548mod tests {
549    // Phase 137: unit tests for old() / validation_error() / has_validation_errors().
550    //
551    // The Request struct wraps hyper::body::Incoming which cannot be constructed
552    // in unit tests.  We therefore test the underlying session-reading logic
553    // directly (the same code path the methods delegate to) using
554    // SESSION_CONTEXT.scope() to inject a session.
555    //
556    // Full end-to-end round-trips (POST → flash → GET → InputProps) live in the
557    // gestiscilo integration test scaffold (validation_roundtrip_tests.rs).
558
559    use crate::session::middleware::SESSION_CONTEXT;
560    use crate::session::store::SessionData;
561    use std::collections::HashMap;
562    use std::sync::Arc;
563    use tokio::sync::RwLock;
564
565    // ── No-session guard tests ────────────────────────────────────────────────
566
567    #[tokio::test]
568    async fn test_session_absent_old_returns_none() {
569        // Outside any SESSION_CONTEXT scope, session() returns None.
570        // old() delegates to session().and_then(...) so it must also return None.
571        let val =
572            crate::session::session().and_then(|s| s.get::<String>("_flash.old._old_input.email"));
573        assert_eq!(val, None);
574    }
575
576    #[tokio::test]
577    async fn test_session_absent_validation_error_returns_none() {
578        let val = crate::session::session().and_then(|s| {
579            s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
580                .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
581        });
582        assert_eq!(val, None);
583    }
584
585    #[tokio::test]
586    async fn test_session_absent_has_validation_errors_false() {
587        let val = crate::session::session()
588            .and_then(|s| s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors"))
589            .map(|m| !m.is_empty())
590            .unwrap_or(false);
591        assert!(!val);
592    }
593
594    // ── Session-present tests (direct logic, mirrors Request method bodies) ───
595
596    #[tokio::test]
597    async fn test_old_reads_from_flash_old_key() {
598        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
599        // Simulate age_flash_data() having moved the flash to _flash.old.*
600        session.put(
601            "_flash.old._old_input.email",
602            "user@example.com".to_string(),
603        );
604
605        let ctx = Arc::new(RwLock::new(Some(session)));
606        let val = SESSION_CONTEXT
607            .scope(ctx, async {
608                crate::session::session()
609                    .and_then(|s| s.get::<String>("_flash.old._old_input.email"))
610            })
611            .await;
612
613        assert_eq!(val, Some("user@example.com".to_string()));
614    }
615
616    #[tokio::test]
617    async fn test_validation_error_reads_first_message_for_field() {
618        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
619        let mut errors: HashMap<String, Vec<String>> = HashMap::new();
620        errors.insert(
621            "email".to_string(),
622            vec!["Inserisci un indirizzo email valido".to_string()],
623        );
624        session.put("_flash.old._validation_errors", &errors);
625
626        let ctx = Arc::new(RwLock::new(Some(session)));
627        let (email_err, other_err) = SESSION_CONTEXT
628            .scope(ctx, async {
629                let email_err = crate::session::session().and_then(|s| {
630                    s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
631                        .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
632                });
633                // Reading the same session twice must not clear the data.
634                let other_err = crate::session::session().and_then(|s| {
635                    s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
636                        .and_then(|map| map.get("name").and_then(|v| v.first()).cloned())
637                });
638                (email_err, other_err)
639            })
640            .await;
641
642        assert_eq!(
643            email_err,
644            Some("Inserisci un indirizzo email valido".to_string())
645        );
646        assert_eq!(other_err, None);
647    }
648
649    #[tokio::test]
650    async fn test_multiple_reads_do_not_clear_flash() {
651        // Validates read-only semantics: calling session().get() twice returns
652        // the same value (unlike get_flash which clears on read).
653        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
654        session.put("_flash.old._old_input.name", "Mario".to_string());
655
656        let ctx = Arc::new(RwLock::new(Some(session)));
657        let (first, second) = SESSION_CONTEXT
658            .scope(ctx, async {
659                let a = crate::session::session()
660                    .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
661                let b = crate::session::session()
662                    .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
663                (a, b)
664            })
665            .await;
666
667        assert_eq!(first, Some("Mario".to_string()));
668        assert_eq!(second, Some("Mario".to_string()));
669    }
670}