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#[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#[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#[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#[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
340pub trait FromRequestHeaders: Sized {
342 type Error: fmt::Display;
343
344 fn from_headers(headers: &HeaderMap) -> Result<Self, Self::Error>;
345}
346
347#[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#[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#[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#[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#[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#[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}