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    /// Request head: method, URI, headers, version, extensions.
13    /// Split out from the original `hyper::Request` so the body can be consumed
14    /// independently via `body_bytes_mut` / `form_mut` / `multipart_mut` etc.
15    parts: hyper::http::request::Parts,
16    /// Request body — either still pending on the wire, cached after a `*_mut`
17    /// read, or taken by a `self`-consuming method (`body_bytes`, `form`, ...).
18    body: BodyState,
19    params: HashMap<String, String>,
20    extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
21    /// Route pattern for metrics (e.g., "/users/{id}" instead of "/users/123")
22    route_pattern: Option<String>,
23    /// Success-side overrides recorded by `req.flash(...)` / `req.redirect_to(...)`.
24    /// Read by the `#[action]` macro runtime after the handler body returns.
25    action_overrides: crate::http::action::ActionOverrides,
26}
27
28/// State of the request body inside a `Request`.
29///
30/// The body can be in one of three states:
31/// - `Pending`: still streaming from the wire (default after `Request::new`).
32/// - `Cached`: collected to memory by a `*_mut` reader (`body_bytes_mut`, etc.).
33///   Multiple `*_mut` calls are safe — they all return the same cached bytes.
34/// - `Consumed`: taken by a `self`-consuming method (`body_bytes`, `form`, ...).
35///   Cannot be read again; the request is typically dropped after.
36///
37/// Mixing `self`-consuming methods with `*_mut` methods on the same request is
38/// safe: after a `*_mut` call caches the body, a subsequent `self`-consuming
39/// method returns the cached bytes; after a `self`-consuming method, the
40/// request is dropped (no `*_mut` call is possible).
41enum BodyState {
42    /// Body is still on the wire — not yet read.
43    Pending(hyper::body::Incoming),
44    /// Body has been collected and cached. Both `*_mut` readers and the legacy
45    /// `self`-consuming `body_bytes` will return clones of these bytes.
46    Cached(Bytes),
47    /// Body was taken by a `self`-consuming method that does not cache (e.g.
48    /// the legacy `Request::into_parts` returning `hyper::body::Incoming`).
49    /// Any subsequent body read returns an error.
50    Consumed,
51}
52
53impl Request {
54    /// Create a new request from a raw hyper request.
55    pub fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
56        let (parts, body) = inner.into_parts();
57        Self {
58            parts,
59            body: BodyState::Pending(body),
60            params: HashMap::new(),
61            extensions: HashMap::new(),
62            route_pattern: None,
63            action_overrides: crate::http::action::ActionOverrides::default(),
64        }
65    }
66
67    /// Attach route parameters extracted from the URL path.
68    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
69        self.params = params;
70        self
71    }
72
73    /// Set the route pattern (e.g., "/users/{id}")
74    pub fn with_route_pattern(mut self, pattern: String) -> Self {
75        self.route_pattern = Some(pattern);
76        self
77    }
78
79    /// Get the route pattern for metrics grouping
80    pub fn route_pattern(&self) -> Option<String> {
81        self.route_pattern.clone()
82    }
83
84    /// Insert a value into the request extensions (type-map pattern)
85    ///
86    /// This is async-safe unlike thread-local storage.
87    pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
88        self.extensions.insert(TypeId::of::<T>(), Box::new(value));
89    }
90
91    /// Get a reference to a value from the request extensions
92    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
93        self.extensions
94            .get(&TypeId::of::<T>())
95            .and_then(|boxed| boxed.downcast_ref::<T>())
96    }
97
98    /// Get a mutable reference to a value from the request extensions
99    pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
100        self.extensions
101            .get_mut(&TypeId::of::<T>())
102            .and_then(|boxed| boxed.downcast_mut::<T>())
103    }
104
105    /// Get the request method
106    pub fn method(&self) -> &hyper::Method {
107        &self.parts.method
108    }
109
110    /// Get the request URI
111    pub fn uri(&self) -> &http::Uri {
112        &self.parts.uri
113    }
114
115    /// Get the request headers
116    pub fn headers(&self) -> &http::HeaderMap {
117        &self.parts.headers
118    }
119
120    /// Get the request path
121    pub fn path(&self) -> &str {
122        self.parts.uri.path()
123    }
124
125    /// Rewrite the request path (server-side only — the browser URL is unchanged).
126    ///
127    /// Replaces the URI path component while preserving the scheme, authority, and
128    /// query string. Used by pre-route middleware (e.g. `HostMiddleware`) to map
129    /// custom-domain requests onto internal slug-based routes before routing occurs.
130    ///
131    /// `new_path` must begin with `/`. Panics in debug mode if it does not.
132    pub fn set_path(&mut self, new_path: &str) {
133        debug_assert!(
134            new_path.starts_with('/'),
135            "set_path: path must begin with '/', got {new_path:?}"
136        );
137        let old_uri = &self.parts.uri;
138        // Preserve scheme, authority, and query string; replace path only.
139        let mut parts = old_uri.clone().into_parts();
140        let path_and_query = match old_uri.query() {
141            Some(q) => format!("{new_path}?{q}"),
142            None => new_path.to_string(),
143        };
144        parts.path_and_query = Some(
145            path_and_query
146                .parse()
147                .unwrap_or_else(|_| new_path.parse().expect("invalid path")),
148        );
149        if let Ok(new_uri) = http::Uri::from_parts(parts) {
150            self.parts.uri = new_uri;
151        }
152    }
153
154    /// Get a route parameter by name (e.g., /users/{id})
155    /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
156    pub fn param(&self, name: &str) -> Result<&str, ParamError> {
157        self.params
158            .get(name)
159            .map(|s| s.as_str())
160            .ok_or_else(|| ParamError {
161                param_name: name.to_string(),
162            })
163    }
164
165    /// Get a route parameter parsed as a specific type
166    ///
167    /// Combines `param()` with parsing, returning a typed value.
168    ///
169    /// # Example
170    ///
171    /// ```rust,ignore
172    /// pub async fn show(req: Request) -> Response {
173    ///     let id: i32 = req.param_as("id")?;
174    ///     // ...
175    /// }
176    /// ```
177    pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Result<T, ParamError>
178    where
179        T::Err: std::fmt::Display,
180    {
181        let value = self.param(name)?;
182        value.parse::<T>().map_err(|e| ParamError {
183            param_name: format!("{name} (parse error: {e})"),
184        })
185    }
186
187    /// Get all route parameters
188    pub fn params(&self) -> &HashMap<String, String> {
189        &self.params
190    }
191
192    /// Get a query string parameter by name
193    ///
194    /// # Example
195    ///
196    /// ```rust,ignore
197    /// // URL: /users?page=2&limit=10
198    /// let page = req.query("page"); // Some("2")
199    /// let sort = req.query("sort"); // None
200    /// ```
201    pub fn query(&self, name: &str) -> Option<String> {
202        self.parts.uri.query().and_then(|q| {
203            form_urlencoded::parse(q.as_bytes())
204                .find(|(key, _)| key == name)
205                .map(|(_, value)| value.into_owned())
206        })
207    }
208
209    /// Get a query string parameter or a default value
210    ///
211    /// # Example
212    ///
213    /// ```rust,ignore
214    /// // URL: /users?page=2
215    /// let page = req.query_or("page", "1"); // "2"
216    /// let limit = req.query_or("limit", "10"); // "10"
217    /// ```
218    pub fn query_or(&self, name: &str, default: &str) -> String {
219        self.query(name).unwrap_or_else(|| default.to_string())
220    }
221
222    /// Get a query string parameter parsed as a specific type
223    ///
224    /// # Example
225    ///
226    /// ```rust,ignore
227    /// // URL: /users?page=2&limit=10
228    /// let page: Option<i32> = req.query_as("page"); // Some(2)
229    /// ```
230    pub fn query_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
231        self.query(name).and_then(|v| v.parse().ok())
232    }
233
234    /// Get a query string parameter parsed as a specific type, or a default
235    ///
236    /// # Example
237    ///
238    /// ```rust,ignore
239    /// // URL: /users?page=2
240    /// let page: i32 = req.query_as_or("page", 1); // 2
241    /// let limit: i32 = req.query_as_or("limit", 10); // 10
242    /// ```
243    pub fn query_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
244        self.query_as(name).unwrap_or(default)
245    }
246
247    // ── Phase 137: validation flash round-trip helpers ────────────────────────
248
249    /// Read a previously-submitted form value from session flash.
250    ///
251    /// After a POST handler calls `ValidationError::with_old_input(&data).redirect_back(...)`,
252    /// the GET handler retrieves the value with `req.old("field_name")` and passes it as
253    /// `InputProps.default_value` to repopulate the form.
254    ///
255    /// Reads from `_flash.old._old_input.<field>` without clearing (read-only semantics).
256    /// Flash aging (move new→old→deleted) is handled by the session middleware at request
257    /// boundaries, so multiple reads in the same GET handler are safe.
258    ///
259    /// Returns `None` when no flash value exists, no session is active, or the key is absent.
260    pub fn old(&self, field: &str) -> Option<String> {
261        let key = format!("_flash.old._old_input.{field}");
262        crate::session::session().and_then(|s| s.get::<String>(&key))
263    }
264
265    /// Read the first validation error message for a field from session flash.
266    ///
267    /// After a POST handler calls `errors.redirect_back(...)`, the GET handler calls
268    /// `req.validation_error("field_name")` and passes the result as `InputProps.error`.
269    ///
270    /// Reads from `_flash.old._validation_errors` without clearing (read-only semantics).
271    ///
272    /// Returns `None` when no flash errors exist, no session is active, or the field has no error.
273    pub fn validation_error(&self, field: &str) -> Option<String> {
274        let errors: Option<std::collections::HashMap<String, Vec<String>>> =
275            crate::session::session().and_then(|s| {
276                s.get::<std::collections::HashMap<String, Vec<String>>>(
277                    "_flash.old._validation_errors",
278                )
279            });
280        errors.and_then(|map| map.get(field).and_then(|v| v.first()).cloned())
281    }
282
283    /// Returns `true` when any validation errors were flashed from a prior request.
284    ///
285    /// Useful for rendering a form-wide error summary banner.
286    pub fn has_validation_errors(&self) -> bool {
287        crate::session::session()
288            .and_then(|s| {
289                s.get::<std::collections::HashMap<String, Vec<String>>>(
290                    "_flash.old._validation_errors",
291                )
292            })
293            .map(|m| !m.is_empty())
294            .unwrap_or(false)
295    }
296
297    /// Get a reference to the request head (method, URI, headers, version).
298    ///
299    /// Previously this method returned `&hyper::Request<hyper::body::Incoming>`.
300    /// The signature changed when the body was split out from the head to support
301    /// `&mut self` body readers — callers that need only headers/URI/method should
302    /// use the dedicated accessors (`uri()`, `headers()`, `method()`); callers
303    /// that need the raw `Parts` for low-level work can use this method.
304    pub fn inner(&self) -> &hyper::http::request::Parts {
305        &self.parts
306    }
307
308    /// Get a header value by name
309    pub fn header(&self, name: &str) -> Option<&str> {
310        self.parts.headers.get(name).and_then(|v| v.to_str().ok())
311    }
312
313    /// Get the Content-Type header
314    pub fn content_type(&self) -> Option<&str> {
315        self.header("content-type")
316    }
317
318    /// Resolve the URL the current request was triggered from, falling
319    /// back to `fallback` when the `Referer` is absent, malformed, or
320    /// points off-origin.
321    ///
322    /// Use to capture a "back" target at handler entry before the request
323    /// body is consumed (e.g. before [`body_bytes`](Self::body_bytes)). The
324    /// returned `String` then feeds [`crate::http::Redirect::to`] to send
325    /// the user back to where they came from, preserving query strings
326    /// (e.g. `?tab=note`) and any other URL state.
327    ///
328    /// Same-origin rule mirrors [`crate::http::Redirect::back`]: absolute
329    /// URLs must share the request's `Host`; scheme-relative URLs are
330    /// rejected.
331    pub fn back_or(&self, fallback: impl Into<String>) -> String {
332        let referer = match self.header("referer") {
333            Some(r) => r,
334            None => return fallback.into(),
335        };
336        if referer.starts_with("//") {
337            return fallback.into();
338        }
339        if referer.starts_with('/') {
340            return referer.to_string();
341        }
342        let rest = match referer
343            .strip_prefix("http://")
344            .or_else(|| referer.strip_prefix("https://"))
345        {
346            Some(r) => r,
347            None => return fallback.into(),
348        };
349        let (referer_host, path) = match rest.find('/') {
350            Some(i) => (&rest[..i], &rest[i..]),
351            None => (rest, "/"),
352        };
353        let request_host = match self.header("host") {
354            Some(h) => h,
355            None => return fallback.into(),
356        };
357        if referer_host == request_host {
358            path.to_string()
359        } else {
360            fallback.into()
361        }
362    }
363
364    /// Check if this is an Inertia XHR request
365    pub fn is_inertia(&self) -> bool {
366        self.header("X-Inertia")
367            .map(|v| v == "true")
368            .unwrap_or(false)
369    }
370
371    /// Get all cookies from the request
372    ///
373    /// Parses the Cookie header and returns a HashMap of cookie names to values.
374    ///
375    /// # Example
376    ///
377    /// ```rust,ignore
378    /// let cookies = req.cookies();
379    /// if let Some(session) = cookies.get("session") {
380    ///     println!("Session: {}", session);
381    /// }
382    /// ```
383    pub fn cookies(&self) -> HashMap<String, String> {
384        self.header("Cookie").map(parse_cookies).unwrap_or_default()
385    }
386
387    /// Get a specific cookie value by name
388    ///
389    /// # Example
390    ///
391    /// ```rust,ignore
392    /// if let Some(session_id) = req.cookie("session") {
393    ///     // Use session_id
394    /// }
395    /// ```
396    pub fn cookie(&self, name: &str) -> Option<String> {
397        self.cookies().get(name).cloned()
398    }
399
400    /// Get the Inertia version from request headers
401    pub fn inertia_version(&self) -> Option<&str> {
402        self.header("X-Inertia-Version")
403    }
404
405    /// Get partial component name for partial reloads
406    pub fn inertia_partial_component(&self) -> Option<&str> {
407        self.header("X-Inertia-Partial-Component")
408    }
409
410    /// Get partial data keys for partial reloads
411    pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
412        self.header("X-Inertia-Partial-Data")
413            .map(|v| v.split(',').collect())
414    }
415
416    /// Consume the request and collect the body as bytes.
417    ///
418    /// If the body has already been read via `body_bytes_mut` (or any other
419    /// `*_mut` body reader), this returns the cached bytes. If the body was
420    /// taken by `into_parts` (the legacy FormRequest extraction path), this
421    /// returns an error.
422    pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
423        let content_type = self
424            .parts
425            .headers
426            .get("content-type")
427            .and_then(|v| v.to_str().ok())
428            .map(|s| s.to_string());
429
430        let params = self.params;
431        let bytes = match self.body {
432            BodyState::Pending(body) => collect_body(body).await?,
433            BodyState::Cached(bytes) => bytes,
434            BodyState::Consumed => {
435                return Err(FrameworkError::internal(
436                    "Request body already consumed — cannot read body_bytes after into_parts",
437                ));
438            }
439        };
440
441        Ok((
442            RequestParts {
443                params,
444                content_type,
445            },
446            bytes,
447        ))
448    }
449
450    /// Parse the request body as JSON
451    ///
452    /// Consumes the request since the body can only be read once.
453    ///
454    /// # Example
455    ///
456    /// ```rust,ignore
457    /// #[derive(Deserialize)]
458    /// struct CreateUser { name: String, email: String }
459    ///
460    /// pub async fn store(req: Request) -> Response {
461    ///     let data: CreateUser = req.json().await?;
462    ///     // ...
463    /// }
464    /// ```
465    pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
466        let (_, bytes) = self.body_bytes().await?;
467        parse_json(&bytes)
468    }
469
470    /// Parse the request body as form-urlencoded
471    ///
472    /// Consumes the request since the body can only be read once.
473    ///
474    /// # Example
475    ///
476    /// ```rust,ignore
477    /// #[derive(Deserialize)]
478    /// struct LoginForm { username: String, password: String }
479    ///
480    /// pub async fn login(req: Request) -> Response {
481    ///     let form: LoginForm = req.form().await?;
482    ///     // ...
483    /// }
484    /// ```
485    pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
486        let (_, bytes) = self.body_bytes().await?;
487        parse_form(&bytes)
488    }
489
490    /// Parse the request body as `multipart/form-data`.
491    ///
492    /// Consumes the request since the body can only be read once.
493    /// The per-field byte cap is read from `UPLOAD_MAX_SIZE_MB` (default 10 MiB),
494    /// and the per-request field cap from `UPLOAD_MAX_FIELDS` (default 100).
495    ///
496    /// # Errors
497    ///
498    /// Returns `FrameworkError::internal(...)` with the literal message
499    /// `"Content-Type is not multipart/form-data or missing boundary"` when
500    /// the request's `Content-Type` header is absent, malformed, or not a
501    /// multipart value.
502    ///
503    /// # Example
504    ///
505    /// ```rust,ignore
506    /// pub async fn upload(req: Request) -> Response {
507    ///     let form = req.multipart().await?;
508    ///     let title = form.field("title").unwrap_or_default();
509    ///     let file = form.file("attachment");
510    ///     // ...
511    /// }
512    /// ```
513    pub async fn multipart(self) -> Result<super::multipart::MultipartForm, FrameworkError> {
514        let content_type = self
515            .parts
516            .headers
517            .get("content-type")
518            .and_then(|v| v.to_str().ok())
519            .map(|s| s.to_string())
520            .unwrap_or_default();
521        match self.body {
522            BodyState::Pending(body) => {
523                super::multipart::parse_multipart_body(
524                    body,
525                    &content_type,
526                    super::multipart::max_file_bytes(),
527                    super::multipart::max_fields(),
528                )
529                .await
530            }
531            BodyState::Cached(bytes) => {
532                super::multipart::parse_multipart_bytes(
533                    bytes,
534                    &content_type,
535                    super::multipart::max_file_bytes(),
536                    super::multipart::max_fields(),
537                )
538                .await
539            }
540            BodyState::Consumed => Err(FrameworkError::internal(
541                "Request body already consumed — cannot read multipart after into_parts",
542            )),
543        }
544    }
545
546    /// Parse the body as multipart/form-data and return the first file
547    /// uploaded under `field`.
548    ///
549    /// Consumes the request since the body can only be read once. Returns
550    /// `Ok(None)` when the multipart body parses successfully but contains
551    /// no file with that field name.
552    ///
553    /// # Example
554    ///
555    /// ```rust,ignore
556    /// pub async fn upload_avatar(req: Request) -> Response {
557    ///     let file = req.file("avatar").await?
558    ///         .ok_or_else(|| FrameworkError::internal("missing avatar"))?;
559    ///     // file.store(&disk, &path).await?;
560    ///     Ok(json!({"size": file.size()}))
561    /// }
562    /// ```
563    pub async fn file(
564        self,
565        field: &str,
566    ) -> Result<Option<super::multipart::UploadedFile>, FrameworkError> {
567        let mut form = self.multipart().await?;
568        Ok(form.files_map.remove(field).and_then(|mut v| {
569            if v.is_empty() {
570                None
571            } else {
572                Some(v.swap_remove(0))
573            }
574        }))
575    }
576
577    /// Parse the request body based on Content-Type header
578    ///
579    /// - `application/json` -> JSON parsing
580    /// - `application/x-www-form-urlencoded` -> Form parsing
581    /// - Otherwise -> JSON parsing (default)
582    ///
583    /// Consumes the request since the body can only be read once.
584    pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
585        let (parts, bytes) = self.body_bytes().await?;
586
587        match parts.content_type.as_deref() {
588            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
589            _ => parse_json(&bytes),
590        }
591    }
592
593    /// Consume the request and return its parts along with the inner hyper request body.
594    ///
595    /// Used internally by the handler macro for FormRequest extraction.
596    /// Panics if the body has already been read by a `*_mut` method or `body_bytes`
597    /// — FormRequest paths must own a fresh hyper body. This is consistent with
598    /// the pre-Phase-180 contract: a request flows into exactly one of the two
599    /// extraction paths.
600    pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
601        let content_type = self
602            .parts
603            .headers
604            .get("content-type")
605            .and_then(|v| v.to_str().ok())
606            .map(|s| s.to_string());
607
608        let params = self.params;
609        let body = match self.body {
610            BodyState::Pending(body) => body,
611            BodyState::Cached(_) => panic!(
612                "Request::into_parts called after body was read via a *_mut method; \
613                 FormRequest extraction requires a fresh hyper body."
614            ),
615            BodyState::Consumed => panic!(
616                "Request::into_parts called twice; FormRequest extraction requires a fresh hyper body."
617            ),
618        };
619
620        (
621            RequestParts {
622                params,
623                content_type,
624            },
625            body,
626        )
627    }
628
629    // ── `*_mut` body readers — usable inside `#[action]`-decorated handlers ──
630    //
631    // The `#[action]` proc-macro binds the user's `req` parameter as
632    // `&mut Request`. The legacy `self`-consuming body readers (`body_bytes`,
633    // `form`, `multipart`, `file`, `json`, `input`) cannot be called on a
634    // mutable reference. The methods below are `&mut self`-compatible
635    // equivalents that cache the body bytes on first read so subsequent
636    // calls return the same payload.
637    //
638    // Each method delegates to `body_bytes_mut` for the actual body collection,
639    // then re-parses the cached bytes for its specific content type. The cache
640    // makes second/third calls a near-zero-cost `Bytes::clone()` (which only
641    // bumps a refcount, no allocation).
642
643    /// Collect the request body as bytes — `&mut self` variant.
644    ///
645    /// First call drains the body from the wire and caches it on `self`.
646    /// Subsequent calls return clones of the cached bytes (refcount bump).
647    /// Returns an error if the body was already taken by `into_parts`.
648    ///
649    /// Use this inside `#[action]`-decorated handlers where `req: &mut Request`.
650    pub async fn body_bytes_mut(&mut self) -> Result<Bytes, FrameworkError> {
651        if let BodyState::Cached(bytes) = &self.body {
652            return Ok(bytes.clone());
653        }
654        // Take ownership of the body state so we can consume the Incoming.
655        let prev = std::mem::replace(&mut self.body, BodyState::Consumed);
656        let bytes = match prev {
657            BodyState::Pending(body) => collect_body(body).await?,
658            BodyState::Cached(bytes) => bytes,
659            BodyState::Consumed => {
660                return Err(FrameworkError::internal(
661                    "Request body already consumed — cannot read body_bytes_mut after into_parts",
662                ));
663            }
664        };
665        self.body = BodyState::Cached(bytes.clone());
666        Ok(bytes)
667    }
668
669    /// Parse the body as JSON — `&mut self` variant.
670    ///
671    /// First call drains and caches; subsequent calls re-parse cached bytes.
672    pub async fn json_mut<T: DeserializeOwned>(&mut self) -> Result<T, FrameworkError> {
673        let bytes = self.body_bytes_mut().await?;
674        parse_json(&bytes)
675    }
676
677    /// Parse the body as form-urlencoded — `&mut self` variant.
678    pub async fn form_mut<T: DeserializeOwned>(&mut self) -> Result<T, FrameworkError> {
679        let bytes = self.body_bytes_mut().await?;
680        parse_form(&bytes)
681    }
682
683    /// Parse the body based on Content-Type — `&mut self` variant.
684    /// Mirrors `input(self)` semantics: form-urlencoded → form, everything else → JSON.
685    pub async fn input_mut<T: DeserializeOwned>(&mut self) -> Result<T, FrameworkError> {
686        let content_type = self
687            .parts
688            .headers
689            .get("content-type")
690            .and_then(|v| v.to_str().ok())
691            .map(|s| s.to_string());
692        let bytes = self.body_bytes_mut().await?;
693        match content_type.as_deref() {
694            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
695            _ => parse_json(&bytes),
696        }
697    }
698
699    /// Parse the body as `multipart/form-data` — `&mut self` variant.
700    ///
701    /// Each call re-parses the multipart structure from the cached bytes, so
702    /// calling this twice returns two independent `MultipartForm` values.
703    /// Per-field and per-request limits read from `UPLOAD_MAX_SIZE_MB`
704    /// and `UPLOAD_MAX_FIELDS` (same as the legacy `multipart(self)`).
705    pub async fn multipart_mut(
706        &mut self,
707    ) -> Result<super::multipart::MultipartForm, FrameworkError> {
708        let content_type = self
709            .parts
710            .headers
711            .get("content-type")
712            .and_then(|v| v.to_str().ok())
713            .map(|s| s.to_string())
714            .unwrap_or_default();
715        let bytes = self.body_bytes_mut().await?;
716        super::multipart::parse_multipart_bytes(
717            bytes,
718            &content_type,
719            super::multipart::max_file_bytes(),
720            super::multipart::max_fields(),
721        )
722        .await
723    }
724
725    /// Parse the body as multipart and return the first file under `field` —
726    /// `&mut self` variant.
727    pub async fn file_mut(
728        &mut self,
729        field: &str,
730    ) -> Result<Option<super::multipart::UploadedFile>, FrameworkError> {
731        let mut form = self.multipart_mut().await?;
732        Ok(form.files_map.remove(field).and_then(|mut v| {
733            if v.is_empty() {
734                None
735            } else {
736                Some(v.swap_remove(0))
737            }
738        }))
739    }
740}
741
742impl Request {
743    /// Record a success-side flash key for the `#[action]` macro runtime to write
744    /// to the session `_action` flash slot when the handler returns `Ok(())`.
745    ///
746    /// Has no observable effect outside an `#[action]`-decorated handler.
747    ///
748    /// # Example
749    ///
750    /// ```rust,ignore
751    /// #[action(redirect_to = "/dashboard/pagine")]
752    /// pub async fn create(req: Request) -> ActionResult {
753    ///     let new_id = Page::create(...).await?;
754    ///     req.redirect_to(format!("/dashboard/pagine/{new_id}"));
755    ///     req.flash("created");
756    ///     Ok(())
757    /// }
758    /// ```
759    pub fn flash(&mut self, key: impl Into<String>) {
760        self.action_overrides.flash = Some(key.into());
761    }
762
763    /// Record a success-side redirect override for the `#[action]` macro runtime
764    /// to apply when the handler returns `Ok(())`. The URL is validated as
765    /// same-origin (must start with `/`) when applied — external URLs are
766    /// silently rejected (T-180-02).
767    ///
768    /// Has no observable effect outside an `#[action]`-decorated handler.
769    pub fn redirect_to(&mut self, url: impl Into<String>) {
770        self.action_overrides.redirect_override = Some(url.into());
771    }
772
773    /// Internal — read by the `#[action]` macro runtime to apply recorded overrides.
774    pub(crate) fn action_overrides(&self) -> &crate::http::action::ActionOverrides {
775        &self.action_overrides
776    }
777}
778
779/// Request parts after body has been separated
780///
781/// Contains metadata needed for body parsing without the body itself.
782#[derive(Clone)]
783pub struct RequestParts {
784    /// Route parameters extracted from the URL path.
785    pub params: HashMap<String, String>,
786    /// Content-Type header value, if present.
787    pub content_type: Option<String>,
788}
789
790#[cfg(test)]
791mod tests {
792    // Phase 137: unit tests for old() / validation_error() / has_validation_errors().
793    //
794    // The Request struct wraps hyper::body::Incoming which cannot be constructed
795    // in unit tests.  We therefore test the underlying session-reading logic
796    // directly (the same code path the methods delegate to) using
797    // SESSION_CONTEXT.scope() to inject a session.
798    //
799    // Full end-to-end round-trips (POST → flash → GET → InputProps) live in the
800    // gestiscilo integration test scaffold (validation_roundtrip_tests.rs).
801
802    use crate::session::middleware::SESSION_CONTEXT;
803    use crate::session::store::SessionData;
804    use std::collections::HashMap;
805    use std::sync::Arc;
806    use tokio::sync::RwLock;
807
808    // ── No-session guard tests ────────────────────────────────────────────────
809
810    #[tokio::test]
811    async fn test_session_absent_old_returns_none() {
812        // Outside any SESSION_CONTEXT scope, session() returns None.
813        // old() delegates to session().and_then(...) so it must also return None.
814        let val =
815            crate::session::session().and_then(|s| s.get::<String>("_flash.old._old_input.email"));
816        assert_eq!(val, None);
817    }
818
819    #[tokio::test]
820    async fn test_session_absent_validation_error_returns_none() {
821        let val = crate::session::session().and_then(|s| {
822            s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
823                .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
824        });
825        assert_eq!(val, None);
826    }
827
828    #[tokio::test]
829    async fn test_session_absent_has_validation_errors_false() {
830        let val = crate::session::session()
831            .and_then(|s| s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors"))
832            .map(|m| !m.is_empty())
833            .unwrap_or(false);
834        assert!(!val);
835    }
836
837    // ── Session-present tests (direct logic, mirrors Request method bodies) ───
838
839    #[tokio::test]
840    async fn test_old_reads_from_flash_old_key() {
841        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
842        // Simulate age_flash_data() having moved the flash to _flash.old.*
843        session.put(
844            "_flash.old._old_input.email",
845            "user@example.com".to_string(),
846        );
847
848        let ctx = Arc::new(RwLock::new(Some(session)));
849        let val = SESSION_CONTEXT
850            .scope(ctx, async {
851                crate::session::session()
852                    .and_then(|s| s.get::<String>("_flash.old._old_input.email"))
853            })
854            .await;
855
856        assert_eq!(val, Some("user@example.com".to_string()));
857    }
858
859    #[tokio::test]
860    async fn test_validation_error_reads_first_message_for_field() {
861        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
862        let mut errors: HashMap<String, Vec<String>> = HashMap::new();
863        errors.insert(
864            "email".to_string(),
865            vec!["Inserisci un indirizzo email valido".to_string()],
866        );
867        session.put("_flash.old._validation_errors", &errors);
868
869        let ctx = Arc::new(RwLock::new(Some(session)));
870        let (email_err, other_err) = SESSION_CONTEXT
871            .scope(ctx, async {
872                let email_err = crate::session::session().and_then(|s| {
873                    s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
874                        .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
875                });
876                // Reading the same session twice must not clear the data.
877                let other_err = crate::session::session().and_then(|s| {
878                    s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
879                        .and_then(|map| map.get("name").and_then(|v| v.first()).cloned())
880                });
881                (email_err, other_err)
882            })
883            .await;
884
885        assert_eq!(
886            email_err,
887            Some("Inserisci un indirizzo email valido".to_string())
888        );
889        assert_eq!(other_err, None);
890    }
891
892    #[tokio::test]
893    async fn test_multiple_reads_do_not_clear_flash() {
894        // Validates read-only semantics: calling session().get() twice returns
895        // the same value (unlike get_flash which clears on read).
896        let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
897        session.put("_flash.old._old_input.name", "Mario".to_string());
898
899        let ctx = Arc::new(RwLock::new(Some(session)));
900        let (first, second) = SESSION_CONTEXT
901            .scope(ctx, async {
902                let a = crate::session::session()
903                    .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
904                let b = crate::session::session()
905                    .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
906                (a, b)
907            })
908            .await;
909
910        assert_eq!(first, Some("Mario".to_string()));
911        assert_eq!(second, Some("Mario".to_string()));
912    }
913}