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}