Skip to main content

rest/
extract.rs

1use actix_multipart::Multipart;
2use actix_web::{
3    dev::Payload,
4    http::{header::HeaderMap, StatusCode},
5    web, Error, FromRequest, HttpRequest, HttpResponse, ResponseError,
6};
7use futures::{
8    future::{ready, LocalBoxFuture, Ready},
9    TryStreamExt,
10};
11use rust_zero_core::{Validate, Violation};
12use serde::{de::DeserializeOwned, Serialize};
13use std::{
14    collections::HashMap,
15    fmt,
16    ops::{Deref, DerefMut},
17    path::{Path, PathBuf},
18};
19use tempfile::TempPath;
20use tokio::io::AsyncWriteExt;
21
22/// Stable, machine-readable failure returned by validated request extractors.
23#[derive(Debug, Serialize)]
24pub struct RequestExtractionError {
25    pub code: &'static str,
26    pub source: &'static str,
27    pub message: String,
28    #[serde(skip_serializing_if = "Vec::is_empty")]
29    pub violations: Vec<Violation>,
30}
31
32impl RequestExtractionError {
33    fn parse(source: &'static str, error: impl fmt::Display) -> Self {
34        Self {
35            code: "invalid_request",
36            source,
37            message: error.to_string(),
38            violations: Vec::new(),
39        }
40    }
41
42    fn validation(source: &'static str, error: rust_zero_core::ValidationErrors) -> Self {
43        Self {
44            code: "validation_failed",
45            source,
46            message: error.to_string(),
47            violations: error.into_violations(),
48        }
49    }
50
51    fn payload_too_large(message: impl Into<String>) -> Self {
52        Self {
53            code: "payload_too_large",
54            source: "multipart",
55            message: message.into(),
56            violations: Vec::new(),
57        }
58    }
59
60    fn internal(source: &'static str, message: impl Into<String>) -> Self {
61        Self {
62            code: "request_storage_failed",
63            source,
64            message: message.into(),
65            violations: Vec::new(),
66        }
67    }
68}
69
70impl fmt::Display for RequestExtractionError {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(formatter, "{} {}: {}", self.source, self.code, self.message)
73    }
74}
75
76impl std::error::Error for RequestExtractionError {}
77
78impl ResponseError for RequestExtractionError {
79    fn status_code(&self) -> StatusCode {
80        match self.code {
81            "payload_too_large" => StatusCode::PAYLOAD_TOO_LARGE,
82            "request_storage_failed" => StatusCode::INTERNAL_SERVER_ERROR,
83            _ => StatusCode::BAD_REQUEST,
84        }
85    }
86
87    fn error_response(&self) -> HttpResponse {
88        HttpResponse::build(self.status_code()).json(self)
89    }
90}
91
92/// Limits and temporary storage settings for [`MultipartForm`] extraction.
93///
94/// File bodies are streamed to randomized temporary files and are removed when the extracted
95/// form is dropped. Applications that need to retain an upload should copy or rename it while
96/// handling the request.
97#[derive(Clone, Debug)]
98pub struct MultipartConfig {
99    pub max_field_bytes: usize,
100    pub max_file_bytes: usize,
101    pub max_total_bytes: usize,
102    pub temp_dir: PathBuf,
103}
104
105impl Default for MultipartConfig {
106    fn default() -> Self {
107        Self {
108            max_field_bytes: 64 * 1024,
109            max_file_bytes: 32 * 1024 * 1024,
110            max_total_bytes: 64 * 1024 * 1024,
111            temp_dir: std::env::temp_dir(),
112        }
113    }
114}
115
116impl MultipartConfig {
117    pub fn new(max_field_bytes: usize, max_file_bytes: usize, max_total_bytes: usize) -> Self {
118        Self {
119            max_field_bytes,
120            max_file_bytes,
121            max_total_bytes,
122            ..Self::default()
123        }
124    }
125
126    pub fn with_temp_dir(mut self, temp_dir: impl Into<PathBuf>) -> Self {
127        self.temp_dir = temp_dir.into();
128        self
129    }
130}
131
132/// A file streamed from a multipart request into temporary storage.
133#[derive(Debug)]
134pub struct UploadedFile {
135    field_name: String,
136    file_name: String,
137    content_type: Option<String>,
138    size: usize,
139    path: TempPath,
140}
141
142impl UploadedFile {
143    pub fn field_name(&self) -> &str {
144        &self.field_name
145    }
146
147    pub fn file_name(&self) -> &str {
148        &self.file_name
149    }
150
151    pub fn content_type(&self) -> Option<&str> {
152        self.content_type.as_deref()
153    }
154
155    pub fn size(&self) -> usize {
156        self.size
157    }
158
159    pub fn path(&self) -> &Path {
160        self.path.as_ref()
161    }
162}
163
164/// Streaming multipart form extractor with bounded text fields, files, and aggregate payloads.
165#[derive(Debug)]
166pub struct MultipartForm {
167    fields: HashMap<String, Vec<String>>,
168    files: Vec<UploadedFile>,
169    total_bytes: usize,
170}
171
172impl MultipartForm {
173    pub fn text(&self, name: &str) -> Option<&str> {
174        self.fields
175            .get(name)
176            .and_then(|values| values.first())
177            .map(String::as_str)
178    }
179
180    pub fn text_values(&self, name: &str) -> &[String] {
181        self.fields.get(name).map(Vec::as_slice).unwrap_or_default()
182    }
183
184    pub fn files(&self) -> &[UploadedFile] {
185        &self.files
186    }
187
188    pub fn files_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a UploadedFile> + 'a {
189        self.files
190            .iter()
191            .filter(move |file| file.field_name == name)
192    }
193
194    pub fn total_bytes(&self) -> usize {
195        self.total_bytes
196    }
197}
198
199impl FromRequest for MultipartForm {
200    type Error = Error;
201    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
202
203    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
204        let config = request
205            .app_data::<web::Data<MultipartConfig>>()
206            .map(|config| config.get_ref().clone())
207            .or_else(|| request.app_data::<MultipartConfig>().cloned())
208            .unwrap_or_default();
209        let extraction = Multipart::from_request(request, payload);
210
211        Box::pin(async move {
212            let mut multipart = extraction
213                .await
214                .map_err(|error| RequestExtractionError::parse("multipart", error))?;
215            let mut fields = HashMap::<String, Vec<String>>::new();
216            let mut files = Vec::new();
217            let mut total_bytes = 0usize;
218
219            while let Some(mut field) = multipart
220                .try_next()
221                .await
222                .map_err(|error| RequestExtractionError::parse("multipart", error))?
223            {
224                let disposition = field.content_disposition();
225                let field_name = disposition
226                    .and_then(|value| value.get_name())
227                    .ok_or_else(|| {
228                        RequestExtractionError::parse(
229                            "multipart",
230                            "part is missing a content-disposition name",
231                        )
232                    })?
233                    .to_owned();
234                let file_name = disposition
235                    .and_then(|value| value.get_filename())
236                    .map(str::to_owned);
237                let content_type = field.content_type().map(ToString::to_string);
238
239                if let Some(file_name) = file_name {
240                    let temporary =
241                        tempfile::NamedTempFile::new_in(&config.temp_dir).map_err(|_| {
242                            RequestExtractionError::internal(
243                                "multipart",
244                                "failed to create temporary upload storage",
245                            )
246                        })?;
247                    let (temporary_file, temporary_path) = temporary.into_parts();
248                    let mut output = tokio::fs::File::from_std(temporary_file);
249                    let mut file_bytes = 0usize;
250
251                    while let Some(chunk) = field
252                        .try_next()
253                        .await
254                        .map_err(|error| RequestExtractionError::parse("multipart", error))?
255                    {
256                        file_bytes = checked_payload_size(
257                            file_bytes,
258                            chunk.len(),
259                            config.max_file_bytes,
260                            "multipart file exceeds the configured limit",
261                        )?;
262                        total_bytes = checked_payload_size(
263                            total_bytes,
264                            chunk.len(),
265                            config.max_total_bytes,
266                            "multipart request exceeds the configured aggregate limit",
267                        )?;
268                        output.write_all(&chunk).await.map_err(|_| {
269                            RequestExtractionError::internal(
270                                "multipart",
271                                "failed to write temporary upload storage",
272                            )
273                        })?;
274                    }
275                    output.flush().await.map_err(|_| {
276                        RequestExtractionError::internal(
277                            "multipart",
278                            "failed to flush temporary upload storage",
279                        )
280                    })?;
281                    files.push(UploadedFile {
282                        field_name,
283                        file_name,
284                        content_type,
285                        size: file_bytes,
286                        path: temporary_path,
287                    });
288                } else {
289                    let mut value = Vec::new();
290                    while let Some(chunk) = field
291                        .try_next()
292                        .await
293                        .map_err(|error| RequestExtractionError::parse("multipart", error))?
294                    {
295                        checked_payload_size(
296                            value.len(),
297                            chunk.len(),
298                            config.max_field_bytes,
299                            "multipart field exceeds the configured limit",
300                        )?;
301                        total_bytes = checked_payload_size(
302                            total_bytes,
303                            chunk.len(),
304                            config.max_total_bytes,
305                            "multipart request exceeds the configured aggregate limit",
306                        )?;
307                        value.extend_from_slice(&chunk);
308                    }
309                    let value = String::from_utf8(value).map_err(|_| {
310                        RequestExtractionError::parse("multipart", "text field is not valid UTF-8")
311                    })?;
312                    fields.entry(field_name).or_default().push(value);
313                }
314            }
315
316            Ok(Self {
317                fields,
318                files,
319                total_bytes,
320            })
321        })
322    }
323}
324
325fn checked_payload_size(
326    current: usize,
327    additional: usize,
328    limit: usize,
329    message: &'static str,
330) -> Result<usize, RequestExtractionError> {
331    let next = current
332        .checked_add(additional)
333        .ok_or_else(|| RequestExtractionError::payload_too_large(message))?;
334    if next > limit {
335        return Err(RequestExtractionError::payload_too_large(message));
336    }
337    Ok(next)
338}
339
340/// Converts request headers into an application type before validation.
341pub trait FromRequestHeaders: Sized {
342    type Error: fmt::Display;
343
344    fn from_headers(headers: &HeaderMap) -> Result<Self, Self::Error>;
345}
346
347/// JSON extractor that runs the request type's [`Validate`] implementation.
348#[derive(Debug)]
349pub struct ValidatedJson<T>(pub T);
350
351impl<T> Deref for ValidatedJson<T> {
352    type Target = T;
353
354    fn deref(&self) -> &Self::Target {
355        &self.0
356    }
357}
358
359impl<T> DerefMut for ValidatedJson<T> {
360    fn deref_mut(&mut self) -> &mut Self::Target {
361        &mut self.0
362    }
363}
364
365impl<T> FromRequest for ValidatedJson<T>
366where
367    T: DeserializeOwned + Validate + 'static,
368{
369    type Error = Error;
370    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
371
372    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
373        let extraction = web::Json::<T>::from_request(request, payload);
374        Box::pin(async move {
375            let value = extraction
376                .await
377                .map_err(|error| RequestExtractionError::parse("body", error))?
378                .into_inner();
379            value
380                .validate()
381                .map_err(|error| RequestExtractionError::validation("body", error))?;
382            Ok(Self(value))
383        })
384    }
385}
386
387/// Query-string extractor that runs the request type's [`Validate`] implementation.
388#[derive(Debug)]
389pub struct ValidatedQuery<T>(pub T);
390
391impl<T> Deref for ValidatedQuery<T> {
392    type Target = T;
393
394    fn deref(&self) -> &Self::Target {
395        &self.0
396    }
397}
398
399/// Header extractor that converts and validates an application-defined type.
400#[derive(Debug)]
401pub struct ValidatedHeader<T>(pub T);
402
403impl<T> Deref for ValidatedHeader<T> {
404    type Target = T;
405
406    fn deref(&self) -> &Self::Target {
407        &self.0
408    }
409}
410
411impl<T> DerefMut for ValidatedHeader<T> {
412    fn deref_mut(&mut self) -> &mut Self::Target {
413        &mut self.0
414    }
415}
416
417impl<T> FromRequest for ValidatedHeader<T>
418where
419    T: FromRequestHeaders + Validate + 'static,
420{
421    type Error = Error;
422    type Future = Ready<Result<Self, Self::Error>>;
423
424    fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
425        let result: Result<Self, Error> = T::from_headers(request.headers())
426            .map_err(|error| Error::from(RequestExtractionError::parse("headers", error)))
427            .and_then(|value| {
428                value.validate().map_err(|error| {
429                    Error::from(RequestExtractionError::validation("headers", error))
430                })?;
431                Ok(Self(value))
432            });
433        ready(result)
434    }
435}
436
437/// Route-path extractor that runs the request type's [`Validate`] implementation.
438#[derive(Debug)]
439pub struct ValidatedPath<T>(pub T);
440
441impl<T> Deref for ValidatedPath<T> {
442    type Target = T;
443
444    fn deref(&self) -> &Self::Target {
445        &self.0
446    }
447}
448
449impl<T> DerefMut for ValidatedPath<T> {
450    fn deref_mut(&mut self) -> &mut Self::Target {
451        &mut self.0
452    }
453}
454
455impl<T> FromRequest for ValidatedPath<T>
456where
457    T: DeserializeOwned + Validate + 'static,
458{
459    type Error = Error;
460    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
461
462    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
463        let extraction = web::Path::<T>::from_request(request, payload);
464        Box::pin(async move {
465            let value = extraction
466                .await
467                .map_err(|error| RequestExtractionError::parse("path", error))?
468                .into_inner();
469            value
470                .validate()
471                .map_err(|error| RequestExtractionError::validation("path", error))?;
472            Ok(Self(value))
473        })
474    }
475}
476
477/// URL-encoded form extractor that runs the request type's [`Validate`] implementation.
478#[derive(Debug)]
479pub struct ValidatedForm<T>(pub T);
480
481impl<T> Deref for ValidatedForm<T> {
482    type Target = T;
483
484    fn deref(&self) -> &Self::Target {
485        &self.0
486    }
487}
488
489impl<T> DerefMut for ValidatedForm<T> {
490    fn deref_mut(&mut self) -> &mut Self::Target {
491        &mut self.0
492    }
493}
494
495impl<T> FromRequest for ValidatedForm<T>
496where
497    T: DeserializeOwned + Validate + 'static,
498{
499    type Error = Error;
500    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
501
502    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
503        let extraction = web::Form::<T>::from_request(request, payload);
504        Box::pin(async move {
505            let value = extraction
506                .await
507                .map_err(|error| RequestExtractionError::parse("form", error))?
508                .into_inner();
509            value
510                .validate()
511                .map_err(|error| RequestExtractionError::validation("form", error))?;
512            Ok(Self(value))
513        })
514    }
515}
516
517impl<T> DerefMut for ValidatedQuery<T> {
518    fn deref_mut(&mut self) -> &mut Self::Target {
519        &mut self.0
520    }
521}
522
523impl<T> FromRequest for ValidatedQuery<T>
524where
525    T: DeserializeOwned + Validate + 'static,
526{
527    type Error = Error;
528    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
529
530    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
531        let extraction = web::Query::<T>::from_request(request, payload);
532        Box::pin(async move {
533            let value = extraction
534                .await
535                .map_err(|error| RequestExtractionError::parse("query", error))?
536                .into_inner();
537            value
538                .validate()
539                .map_err(|error| RequestExtractionError::validation("query", error))?;
540            Ok(Self(value))
541        })
542    }
543}
544
545/// A request parsed and validated from its path, query, headers, and JSON body.
546///
547/// This extractor keeps each transport source in a separate application type, avoiding
548/// ambiguous precedence when the same field name appears in more than one source.
549#[derive(Debug)]
550pub struct ValidatedRequest<P, Q, H, B> {
551    pub path: P,
552    pub query: Q,
553    pub headers: H,
554    pub body: B,
555}
556
557impl<P, Q, H, B> FromRequest for ValidatedRequest<P, Q, H, B>
558where
559    P: DeserializeOwned + Validate + 'static,
560    Q: DeserializeOwned + Validate + 'static,
561    H: FromRequestHeaders + Validate + 'static,
562    B: DeserializeOwned + Validate + 'static,
563{
564    type Error = Error;
565    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
566
567    fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
568        let path = ValidatedPath::<P>::from_request(request, payload);
569        let query = ValidatedQuery::<Q>::from_request(request, payload);
570        let headers = ValidatedHeader::<H>::from_request(request, payload);
571        let body = ValidatedJson::<B>::from_request(request, payload);
572
573        Box::pin(async move {
574            Ok(Self {
575                path: path.await?.0,
576                query: query.await?.0,
577                headers: headers.await?.0,
578                body: body.await?.0,
579            })
580        })
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587    use actix_web::{
588        http::{header::HeaderMap, StatusCode},
589        test, web, App, HttpResponse,
590    };
591    use rust_zero_core::{Validation, ValidationErrors};
592    use serde::Deserialize;
593
594    fn multipart_body(boundary: &str, title: &str, file: &[u8]) -> Vec<u8> {
595        let mut body = format!(
596            "--{boundary}\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n{title}\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"asset\"; filename=\"note.txt\"\r\nContent-Type: text/plain\r\n\r\n"
597        )
598        .into_bytes();
599        body.extend_from_slice(file);
600        body.extend_from_slice(format!("\r\n--{boundary}--\r\n").as_bytes());
601        body
602    }
603
604    #[derive(Deserialize)]
605    struct Request {
606        name: String,
607    }
608
609    #[derive(Deserialize)]
610    struct NumberRequest {
611        value: u64,
612    }
613
614    impl Validate for NumberRequest {
615        fn validate(&self) -> Result<(), ValidationErrors> {
616            Ok(())
617        }
618    }
619
620    impl Validate for Request {
621        fn validate(&self) -> Result<(), ValidationErrors> {
622            let mut validation = Validation::new();
623            validation.required("name", &self.name);
624            validation.finish()
625        }
626    }
627
628    impl FromRequestHeaders for Request {
629        type Error = &'static str;
630
631        fn from_headers(headers: &HeaderMap) -> Result<Self, Self::Error> {
632            let name = headers
633                .get("x-name")
634                .ok_or("x-name is required")?
635                .to_str()
636                .map_err(|_| "x-name must be text")?;
637            Ok(Self {
638                name: name.to_owned(),
639            })
640        }
641    }
642
643    #[actix_web::test]
644    async fn rejects_invalid_json_before_the_handler() {
645        let app = test::init_service(App::new().route(
646            "/",
647            web::post().to(|_: ValidatedJson<Request>| async { HttpResponse::Ok().finish() }),
648        ))
649        .await;
650
651        let request = test::TestRequest::post()
652            .uri("/")
653            .set_json(serde_json::json!({ "name": " " }))
654            .to_request();
655        let response = test::call_service(&app, request).await;
656
657        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
658        let body: serde_json::Value = test::read_body_json(response).await;
659        assert_eq!(body["code"], "validation_failed");
660        assert_eq!(body["source"], "body");
661        assert_eq!(body["violations"][0]["field"], "name");
662    }
663
664    #[actix_web::test]
665    async fn extracts_and_validates_typed_headers() {
666        let app = test::init_service(App::new().route(
667            "/",
668            web::get().to(|value: ValidatedHeader<Request>| async move {
669                HttpResponse::Ok().body(value.name.clone())
670            }),
671        ))
672        .await;
673
674        let response = test::call_service(
675            &app,
676            test::TestRequest::get()
677                .uri("/")
678                .insert_header(("x-name", "Ada"))
679                .to_request(),
680        )
681        .await;
682        assert_eq!(test::read_body(response).await, "Ada");
683
684        let response = test::call_service(
685            &app,
686            test::TestRequest::get()
687                .uri("/")
688                .insert_header(("x-name", " "))
689                .to_request(),
690        )
691        .await;
692        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
693        let body: serde_json::Value = test::read_body_json(response).await;
694        assert_eq!(body["source"], "headers");
695        assert_eq!(body["violations"][0]["code"], "required");
696    }
697
698    #[actix_web::test]
699    async fn accepts_valid_json_and_query_values() {
700        let app = test::init_service(
701            App::new()
702                .route(
703                    "/json",
704                    web::post().to(|value: ValidatedJson<Request>| async move {
705                        HttpResponse::Ok().body(value.name.clone())
706                    }),
707                )
708                .route(
709                    "/query",
710                    web::get().to(|value: ValidatedQuery<Request>| async move {
711                        HttpResponse::Ok().body(value.name.clone())
712                    }),
713                ),
714        )
715        .await;
716
717        let json_response = test::call_service(
718            &app,
719            test::TestRequest::post()
720                .uri("/json")
721                .set_json(serde_json::json!({ "name": "Ada" }))
722                .to_request(),
723        )
724        .await;
725        assert_eq!(test::read_body(json_response).await, "Ada");
726
727        let query_response = test::call_service(
728            &app,
729            test::TestRequest::get()
730                .uri("/query?name=Grace")
731                .to_request(),
732        )
733        .await;
734        assert_eq!(test::read_body(query_response).await, "Grace");
735    }
736
737    #[actix_web::test]
738    async fn rejects_invalid_queries_before_the_handler() {
739        let app = test::init_service(App::new().route(
740            "/",
741            web::get().to(|_: ValidatedQuery<Request>| async { HttpResponse::Ok().finish() }),
742        ))
743        .await;
744        let response = test::call_service(
745            &app,
746            test::TestRequest::get().uri("/?name=%20").to_request(),
747        )
748        .await;
749
750        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
751    }
752
753    #[actix_web::test]
754    async fn validates_path_and_form_values() {
755        let app = test::init_service(
756            App::new()
757                .route(
758                    "/path/{name}",
759                    web::get().to(|value: ValidatedPath<Request>| async move {
760                        HttpResponse::Ok().body(value.name.clone())
761                    }),
762                )
763                .route(
764                    "/form",
765                    web::post().to(|value: ValidatedForm<Request>| async move {
766                        HttpResponse::Ok().body(value.name.clone())
767                    }),
768                ),
769        )
770        .await;
771
772        let path_response =
773            test::call_service(&app, test::TestRequest::get().uri("/path/Ada").to_request()).await;
774        assert_eq!(test::read_body(path_response).await, "Ada");
775
776        let invalid_path =
777            test::call_service(&app, test::TestRequest::get().uri("/path/%20").to_request()).await;
778        assert_eq!(invalid_path.status(), StatusCode::BAD_REQUEST);
779
780        let form_response = test::call_service(
781            &app,
782            test::TestRequest::post()
783                .uri("/form")
784                .insert_header(("content-type", "application/x-www-form-urlencoded"))
785                .set_payload("name=Grace")
786                .to_request(),
787        )
788        .await;
789        assert_eq!(test::read_body(form_response).await, "Grace");
790
791        let invalid_form = test::call_service(
792            &app,
793            test::TestRequest::post()
794                .uri("/form")
795                .insert_header(("content-type", "application/x-www-form-urlencoded"))
796                .set_payload("name=%20")
797                .to_request(),
798        )
799        .await;
800        assert_eq!(invalid_form.status(), StatusCode::BAD_REQUEST);
801    }
802
803    #[actix_web::test]
804    async fn streams_multipart_files_and_cleans_temporary_storage() {
805        let temp_dir = tempfile::tempdir().unwrap();
806        let app = test::init_service(
807            App::new()
808                .app_data(web::Data::new(
809                    MultipartConfig::new(32, 32, 64).with_temp_dir(temp_dir.path()),
810                ))
811                .route(
812                    "/",
813                    web::post().to(|form: MultipartForm| async move {
814                        let file = &form.files()[0];
815                        let contents = tokio::fs::read(file.path()).await.unwrap();
816                        HttpResponse::Ok().json(serde_json::json!({
817                            "title": form.text("title"),
818                            "file_name": file.file_name(),
819                            "content_type": file.content_type(),
820                            "size": file.size(),
821                            "total": form.total_bytes(),
822                            "contents": String::from_utf8(contents).unwrap(),
823                            "path": file.path(),
824                        }))
825                    }),
826                ),
827        )
828        .await;
829
830        let boundary = "rust-zero-boundary";
831        let response = test::call_service(
832            &app,
833            test::TestRequest::post()
834                .uri("/")
835                .insert_header((
836                    "content-type",
837                    format!("multipart/form-data; boundary={boundary}"),
838                ))
839                .set_payload(multipart_body(boundary, "hello", b"upload"))
840                .to_request(),
841        )
842        .await;
843
844        assert_eq!(response.status(), StatusCode::OK);
845        let body: serde_json::Value = test::read_body_json(response).await;
846        assert_eq!(body["title"], "hello");
847        assert_eq!(body["file_name"], "note.txt");
848        assert_eq!(body["content_type"], "text/plain");
849        assert_eq!(body["size"], 6);
850        assert_eq!(body["total"], 11);
851        assert_eq!(body["contents"], "upload");
852        assert!(!Path::new(body["path"].as_str().unwrap()).exists());
853    }
854
855    #[actix_web::test]
856    async fn enforces_each_multipart_size_limit_without_leaking_files() {
857        async fn call_with_limits(
858            config: MultipartConfig,
859            body: Vec<u8>,
860        ) -> (StatusCode, serde_json::Value) {
861            let app = test::init_service(App::new().app_data(web::Data::new(config)).route(
862                "/",
863                web::post().to(|_: MultipartForm| async { HttpResponse::Ok().finish() }),
864            ))
865            .await;
866            let response = test::call_service(
867                &app,
868                test::TestRequest::post()
869                    .uri("/")
870                    .insert_header((
871                        "content-type",
872                        "multipart/form-data; boundary=limit-boundary",
873                    ))
874                    .set_payload(body)
875                    .to_request(),
876            )
877            .await;
878            let status = response.status();
879            (status, test::read_body_json(response).await)
880        }
881
882        let temp_dir = tempfile::tempdir().unwrap();
883        let field = call_with_limits(
884            MultipartConfig::new(3, 32, 64).with_temp_dir(temp_dir.path()),
885            multipart_body("limit-boundary", "four", b"ok"),
886        )
887        .await;
888        assert_eq!(field.0, StatusCode::PAYLOAD_TOO_LARGE);
889        assert_eq!(field.1["code"], "payload_too_large");
890
891        let file = call_with_limits(
892            MultipartConfig::new(32, 3, 64).with_temp_dir(temp_dir.path()),
893            multipart_body("limit-boundary", "ok", b"four"),
894        )
895        .await;
896        assert_eq!(file.0, StatusCode::PAYLOAD_TOO_LARGE);
897
898        let aggregate = call_with_limits(
899            MultipartConfig::new(32, 32, 5).with_temp_dir(temp_dir.path()),
900            multipart_body("limit-boundary", "abc", b"def"),
901        )
902        .await;
903        assert_eq!(aggregate.0, StatusCode::PAYLOAD_TOO_LARGE);
904        assert_eq!(std::fs::read_dir(temp_dir.path()).unwrap().count(), 0);
905    }
906
907    #[actix_web::test]
908    async fn wrappers_support_mutable_dereferencing() {
909        let mut json = ValidatedJson(Request {
910            name: "before".to_owned(),
911        });
912        json.name = "after".to_owned();
913        assert_eq!(json.name, "after");
914
915        let mut query = ValidatedQuery(Request {
916            name: "before".to_owned(),
917        });
918        query.name = "after".to_owned();
919        assert_eq!(query.name, "after");
920
921        let mut path = ValidatedPath(Request {
922            name: "before".to_owned(),
923        });
924        path.name = "after".to_owned();
925        assert_eq!(path.name, "after");
926
927        let mut form = ValidatedForm(Request {
928            name: "before".to_owned(),
929        });
930        form.name = "after".to_owned();
931        assert_eq!(form.name, "after");
932
933        let mut header = ValidatedHeader(Request {
934            name: "before".to_owned(),
935        });
936        header.name = "after".to_owned();
937        assert_eq!(header.name, "after");
938    }
939
940    #[actix_web::test]
941    async fn combines_all_request_sources_without_field_precedence() {
942        let app =
943            test::init_service(
944                App::new().route(
945                    "/users/{name}",
946                    web::post().to(
947                        |request: ValidatedRequest<
948                            Request,
949                            NumberRequest,
950                            Request,
951                            NumberRequest,
952                        >| async move {
953                            HttpResponse::Ok().json(serde_json::json!({
954                                "path": request.path.name,
955                                "query": request.query.value,
956                                "header": request.headers.name,
957                                "body": request.body.value,
958                            }))
959                        },
960                    ),
961                ),
962            )
963            .await;
964
965        let response = test::call_service(
966            &app,
967            test::TestRequest::post()
968                .uri("/users/Ada?value=7")
969                .insert_header(("x-name", "Grace"))
970                .set_json(serde_json::json!({ "value": 11 }))
971                .to_request(),
972        )
973        .await;
974
975        assert_eq!(response.status(), StatusCode::OK);
976        let body: serde_json::Value = test::read_body_json(response).await;
977        assert_eq!(
978            body,
979            serde_json::json!({
980                "path": "Ada",
981                "query": 7,
982                "header": "Grace",
983                "body": 11,
984            })
985        );
986    }
987}