1use crate::Request;
19use crate::service::web::extract::FromRequest;
20use crate::utils::macros::{composite_http_rejection, define_http_rejection};
21use ahash::HashMap;
22use rama_core::bytes::Bytes;
23use rama_core::extensions::{Extension, ExtensionsRef};
24use rama_core::futures::{Stream, TryStream};
25use rama_http_types::{HeaderMap, StatusCode, header};
26use rama_utils::macros::generate_set_and_with;
27use std::borrow::Cow;
28use std::marker::PhantomData;
29use std::pin::Pin;
30use std::task::{Context, Poll};
31
32#[derive(Debug, Clone, Default, Extension)]
48#[extension(tags(http))]
49pub struct MultipartConfig {
50 default_field_limit: Option<u64>,
51 field_limits: HashMap<Cow<'static, str>, u64>,
52}
53
54impl MultipartConfig {
55 #[must_use]
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 generate_set_and_with! {
63 pub fn default_field_limit(mut self, limit: Option<u64>) -> Self {
66 self.default_field_limit = limit;
67 self
68 }
69 }
70
71 #[must_use]
73 pub fn with_field_limit(mut self, name: impl Into<Cow<'static, str>>, limit: u64) -> Self {
74 self.field_limits.insert(name.into(), limit);
75 self
76 }
77
78 pub fn set_field_limit(&mut self, name: impl Into<Cow<'static, str>>, limit: u64) -> &mut Self {
80 self.field_limits.insert(name.into(), limit);
81 self
82 }
83
84 pub fn merge_floor(&mut self, other: &Self) {
87 if let Some(other_default) = other.default_field_limit {
88 self.default_field_limit = Some(match self.default_field_limit {
89 Some(cur) => cur.min(other_default),
90 None => other_default,
91 });
92 }
93 for (name, &other_limit) in &other.field_limits {
94 self.field_limits
95 .entry(name.clone())
96 .and_modify(|cur| *cur = (*cur).min(other_limit))
97 .or_insert(other_limit);
98 }
99 }
100
101 fn to_constraints(&self) -> multer::Constraints {
109 let mut limit = multer::SizeLimit::new();
110 if let Some(default) = self.default_field_limit {
111 limit = limit.per_field(default);
112 }
113 for (name, &n) in &self.field_limits {
114 limit = limit.for_field(name.as_ref().to_owned(), n);
115 }
116 multer::Constraints::new().size_limit(limit)
117 }
118}
119
120#[derive(Debug)]
136pub struct Multipart {
137 inner: multer::Multipart<'static>,
138}
139
140impl Multipart {
141 pub fn from_body(body: crate::Body, boundary: impl Into<String>) -> Self {
144 let stream = body.into_data_stream();
145 let inner = multer::Multipart::new(stream, boundary);
146 Self { inner }
147 }
148
149 pub fn from_body_with_config(
153 body: crate::Body,
154 boundary: impl Into<String>,
155 config: Option<&MultipartConfig>,
156 ) -> Self {
157 let stream = body.into_data_stream();
158 let inner = match config {
159 Some(c) => multer::Multipart::with_constraints(stream, boundary, c.to_constraints()),
160 None => multer::Multipart::new(stream, boundary),
161 };
162 Self { inner }
163 }
164
165 pub fn from_stream_with_config<S, O, E, B>(
169 stream: S,
170 boundary: B,
171 config: Option<&MultipartConfig>,
172 ) -> Self
173 where
174 S: Stream<Item = Result<O, E>> + Send + 'static,
175 O: Into<Bytes> + 'static,
176 E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
177 B: Into<String>,
178 {
179 let inner = match config {
180 Some(c) => multer::Multipart::with_constraints(stream, boundary, c.to_constraints()),
181 None => multer::Multipart::new(stream, boundary),
182 };
183 Self { inner }
184 }
185
186 pub async fn next_field(&mut self) -> Result<Option<Field<'_>>, MultipartError> {
192 match self.inner.next_field().await {
193 Ok(Some(inner)) => Ok(Some(Field {
194 inner,
195 _marker: PhantomData,
196 })),
197 Ok(None) => Ok(None),
198 Err(err) => Err(MultipartError::from(err)),
199 }
200 }
201}
202
203#[derive(Debug)]
209pub struct Field<'a> {
210 inner: multer::Field<'static>,
211 _marker: PhantomData<&'a mut Multipart>,
212}
213
214impl Field<'_> {
215 #[must_use]
217 pub fn name(&self) -> Option<&str> {
218 self.inner.name()
219 }
220
221 #[must_use]
223 pub fn file_name(&self) -> Option<&str> {
224 self.inner.file_name()
225 }
226
227 #[must_use]
233 pub fn content_type(&self) -> Option<&crate::mime::Mime> {
234 self.inner.content_type()
235 }
236
237 pub fn headers(&self) -> Result<HeaderMap, rama_http_types::Error> {
242 rama_http_hyperium::TryIntoRamaHttp::try_into_rama_http(self.inner.headers())
243 }
244
245 #[must_use]
247 pub fn index(&self) -> usize {
248 self.inner.index()
249 }
250
251 pub async fn bytes(self) -> Result<Bytes, MultipartError> {
253 self.inner.bytes().await.map_err(MultipartError::from)
254 }
255
256 pub async fn text(self) -> Result<String, MultipartError> {
259 self.inner.text().await.map_err(MultipartError::from)
260 }
261
262 pub async fn chunk(&mut self) -> Result<Option<Bytes>, MultipartError> {
264 self.inner.chunk().await.map_err(MultipartError::from)
265 }
266}
267
268impl Stream for Field<'_> {
269 type Item = Result<Bytes, MultipartError>;
270
271 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
272 let this = self.get_mut();
273 let inner = Pin::new(&mut this.inner);
274 inner
275 .try_poll_next(cx)
276 .map(|opt| opt.map(|res| res.map_err(MultipartError::from)))
277 }
278}
279
280#[derive(Debug)]
287pub struct MultipartError {
288 source: multer::Error,
289}
290
291impl MultipartError {
292 #[must_use]
294 pub fn status(&self) -> StatusCode {
295 match &self.source {
296 multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. } => {
297 StatusCode::PAYLOAD_TOO_LARGE
298 }
299 multer::Error::StreamReadFailed(_) | multer::Error::LockFailure => {
300 StatusCode::INTERNAL_SERVER_ERROR
301 }
302 _ => StatusCode::BAD_REQUEST,
303 }
304 }
305
306 #[must_use]
308 pub fn body_text(&self) -> String {
309 format!(
310 "Failed to parse `multipart/form-data` request: {}",
311 self.source
312 )
313 }
314}
315
316impl From<multer::Error> for MultipartError {
317 fn from(source: multer::Error) -> Self {
318 Self { source }
319 }
320}
321
322impl std::fmt::Display for MultipartError {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 write!(f, "Error parsing `multipart/form-data` request")
325 }
326}
327
328impl std::error::Error for MultipartError {
329 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
330 Some(&self.source)
331 }
332}
333
334impl crate::service::web::endpoint::IntoResponse for MultipartError {
335 fn into_response(self) -> crate::Response {
336 crate::utils::macros::log_http_rejection!(
337 rejection_type = MultipartError,
338 body_text = self.body_text(),
339 status = self.status(),
340 );
341 (self.status(), self.body_text()).into_response()
342 }
343}
344
345define_http_rejection! {
346 #[status = BAD_REQUEST]
347 #[body = "Multipart request is missing or has an invalid `Content-Type` boundary"]
348 pub struct InvalidMultipartBoundary;
351}
352
353define_http_rejection! {
354 #[status = UNSUPPORTED_MEDIA_TYPE]
355 #[body = "Multipart requests must have `Content-Type: multipart/form-data`"]
356 pub struct InvalidMultipartContentType;
359}
360
361composite_http_rejection! {
362 pub enum MultipartRejection {
364 InvalidMultipartContentType,
365 InvalidMultipartBoundary,
366 MultipartError,
367 }
368}
369
370impl FromRequest for Multipart {
371 type Rejection = MultipartRejection;
372
373 async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
374 let content_type = req
375 .headers()
376 .get(header::CONTENT_TYPE)
377 .and_then(|v| v.to_str().ok())
378 .ok_or(InvalidMultipartContentType)?;
379
380 if !is_multipart_form_data(content_type) {
385 return Err(InvalidMultipartContentType.into());
386 }
387 let boundary =
388 multer::parse_boundary(content_type).map_err(|_e| InvalidMultipartBoundary)?;
389
390 let config = req.extensions().get_arc::<MultipartConfig>();
394 Ok(Self::from_body_with_config(
395 req.into_body(),
396 boundary,
397 config.as_deref(),
398 ))
399 }
400}
401
402fn is_multipart_form_data(content_type: &str) -> bool {
403 content_type
404 .parse::<crate::mime::Mime>()
405 .ok()
406 .is_some_and(|m| {
407 m.type_() == crate::mime::MULTIPART && m.subtype() == crate::mime::FORM_DATA
408 })
409}
410
411#[cfg(test)]
412mod test {
413 use super::*;
414 use crate::StatusCode;
415 use crate::service::web::WebService;
416 use rama_core::Service;
417 use rama_utils::octets::kib_u64;
418
419 const BOUNDARY: &str = "X-RAMA-TEST-BOUNDARY";
420
421 fn body_with(parts: &[(&str, Option<&str>, Option<&str>, &[u8])]) -> Vec<u8> {
422 let mut out = Vec::new();
423 for (name, file_name, content_type, data) in parts {
424 out.extend_from_slice(b"--");
425 out.extend_from_slice(BOUNDARY.as_bytes());
426 out.extend_from_slice(b"\r\nContent-Disposition: form-data; name=\"");
427 out.extend_from_slice(name.as_bytes());
428 out.extend_from_slice(b"\"");
429 if let Some(fname) = file_name {
430 out.extend_from_slice(b"; filename=\"");
431 out.extend_from_slice(fname.as_bytes());
432 out.extend_from_slice(b"\"");
433 }
434 out.extend_from_slice(b"\r\n");
435 if let Some(ct) = content_type {
436 out.extend_from_slice(b"Content-Type: ");
437 out.extend_from_slice(ct.as_bytes());
438 out.extend_from_slice(b"\r\n");
439 }
440 out.extend_from_slice(b"\r\n");
441 out.extend_from_slice(data);
442 out.extend_from_slice(b"\r\n");
443 }
444 out.extend_from_slice(b"--");
445 out.extend_from_slice(BOUNDARY.as_bytes());
446 out.extend_from_slice(b"--\r\n");
447 out
448 }
449
450 fn ct() -> String {
451 format!("multipart/form-data; boundary={BOUNDARY}")
452 }
453
454 #[tokio::test]
455 async fn test_multipart_text_and_file() {
456 let service =
457 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
458 let f1 = mp.next_field().await.unwrap().unwrap();
459 assert_eq!(f1.name(), Some("name"));
460 assert_eq!(f1.file_name(), None);
461 assert_eq!(f1.text().await.unwrap(), "glen");
462
463 let f2 = mp.next_field().await.unwrap().unwrap();
464 assert_eq!(f2.name(), Some("avatar"));
465 assert_eq!(f2.file_name(), Some("a.bin"));
466 assert_eq!(
467 f2.content_type().map(|m| m.essence_str()),
468 Some("application/octet-stream")
469 );
470 assert_eq!(f2.bytes().await.unwrap().as_ref(), b"\x00\x01\x02");
471
472 assert!(mp.next_field().await.unwrap().is_none());
473 StatusCode::OK
474 });
475
476 let body = body_with(&[
477 ("name", None, None, b"glen"),
478 (
479 "avatar",
480 Some("a.bin"),
481 Some("application/octet-stream"),
482 &[0, 1, 2],
483 ),
484 ]);
485 let req = rama_http_types::Request::builder()
486 .method(rama_http_types::Method::POST)
487 .header(rama_http_types::header::CONTENT_TYPE, ct())
488 .body(body.into())
489 .unwrap();
490 let resp = service.serve(req).await.unwrap();
491 assert_eq!(resp.status(), StatusCode::OK);
492 }
493
494 #[tokio::test]
495 async fn test_multipart_quoted_boundary_in_content_type() {
496 let service =
501 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
502 let f = mp.next_field().await.unwrap().unwrap();
503 assert_eq!(f.text().await.unwrap(), "v");
504 StatusCode::OK
505 });
506
507 let body = body_with(&[("k", None, None, b"v")]);
508 let req = rama_http_types::Request::builder()
509 .method(rama_http_types::Method::POST)
510 .header(
511 rama_http_types::header::CONTENT_TYPE,
512 format!("multipart/form-data; boundary=\"{BOUNDARY}\""),
513 )
514 .body(body.into())
515 .unwrap();
516 let resp = service.serve(req).await.unwrap();
517 assert_eq!(resp.status(), StatusCode::OK);
518 }
519
520 #[tokio::test]
521 async fn test_multipart_with_preamble_and_epilogue() {
522 let mut body = Vec::new();
526 body.extend_from_slice(b"This is a preamble that must be ignored.\r\n");
527 body.extend_from_slice(&body_with(&[("k", None, None, b"v")]));
528 body.extend_from_slice(b"trailing epilogue ignored\r\n");
529
530 let service =
531 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
532 let f = mp.next_field().await.unwrap().unwrap();
533 assert_eq!(f.name(), Some("k"));
534 assert_eq!(f.text().await.unwrap(), "v");
535 StatusCode::OK
536 });
537
538 let req = rama_http_types::Request::builder()
539 .method(rama_http_types::Method::POST)
540 .header(rama_http_types::header::CONTENT_TYPE, ct())
541 .body(body.into())
542 .unwrap();
543 let resp = service.serve(req).await.unwrap();
544 assert_eq!(resp.status(), StatusCode::OK);
545 }
546
547 #[tokio::test]
548 async fn test_multipart_with_transport_padding() {
549 let mut body = Vec::new();
553 body.extend_from_slice(b"--");
554 body.extend_from_slice(BOUNDARY.as_bytes());
555 body.extend_from_slice(b" \r\n"); body.extend_from_slice(b"Content-Disposition: form-data; name=\"k\"\r\n\r\n");
557 body.extend_from_slice(b"v\r\n");
558 body.extend_from_slice(b"--");
559 body.extend_from_slice(BOUNDARY.as_bytes());
560 body.extend_from_slice(b"--\t \r\n"); let service =
563 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
564 let f = mp.next_field().await.unwrap().unwrap();
565 assert_eq!(f.text().await.unwrap(), "v");
566 StatusCode::OK
567 });
568
569 let req = rama_http_types::Request::builder()
570 .method(rama_http_types::Method::POST)
571 .header(rama_http_types::header::CONTENT_TYPE, ct())
572 .body(body.into())
573 .unwrap();
574 let resp = service.serve(req).await.unwrap();
575 assert_eq!(resp.status(), StatusCode::OK);
576 }
577
578 #[tokio::test]
579 async fn test_multipart_filename_with_rfc5987_ext_value() {
580 let mut body = Vec::new();
589 body.extend_from_slice(b"--");
590 body.extend_from_slice(BOUNDARY.as_bytes());
591 body.extend_from_slice(b"\r\n");
592 body.extend_from_slice(
593 b"Content-Disposition: form-data; name=\"file\"; filename*=UTF-8''r%C3%A9sum%C3%A9.txt\r\n",
594 );
595 body.extend_from_slice(b"\r\n");
596 body.extend_from_slice(b"hello\r\n");
597 body.extend_from_slice(b"--");
598 body.extend_from_slice(BOUNDARY.as_bytes());
599 body.extend_from_slice(b"--\r\n");
600
601 let service =
602 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
603 let f = mp.next_field().await.unwrap().unwrap();
604 assert_eq!(f.name(), Some("file"));
605 assert_eq!(f.file_name(), None);
607 assert_eq!(f.text().await.unwrap(), "hello");
608 StatusCode::OK
609 });
610
611 let req = rama_http_types::Request::builder()
612 .method(rama_http_types::Method::POST)
613 .header(rama_http_types::header::CONTENT_TYPE, ct())
614 .body(body.into())
615 .unwrap();
616 let resp = service.serve(req).await.unwrap();
617 assert_eq!(resp.status(), StatusCode::OK);
618 }
619
620 #[tokio::test]
621 async fn test_multipart_filename_utf8_passthrough() {
622 let mut body = Vec::new();
627 body.extend_from_slice(b"--");
628 body.extend_from_slice(BOUNDARY.as_bytes());
629 body.extend_from_slice(b"\r\n");
630 body.extend_from_slice(
632 "Content-Disposition: form-data; name=\"file\"; filename=\"résumé.txt\"\r\n".as_bytes(),
633 );
634 body.extend_from_slice(b"\r\n");
635 body.extend_from_slice(b"hello\r\n");
636 body.extend_from_slice(b"--");
637 body.extend_from_slice(BOUNDARY.as_bytes());
638 body.extend_from_slice(b"--\r\n");
639
640 let service =
641 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
642 let f = mp.next_field().await.unwrap().unwrap();
643 assert_eq!(f.name(), Some("file"));
644 assert_eq!(f.file_name(), Some("résumé.txt"));
645 assert_eq!(f.text().await.unwrap(), "hello");
646 StatusCode::OK
647 });
648
649 let req = rama_http_types::Request::builder()
650 .method(rama_http_types::Method::POST)
651 .header(rama_http_types::header::CONTENT_TYPE, ct())
652 .body(body.into())
653 .unwrap();
654 let resp = service.serve(req).await.unwrap();
655 assert_eq!(resp.status(), StatusCode::OK);
656 }
657
658 #[tokio::test]
659 async fn test_multipart_filename_with_escaped_quote() {
660 let mut body = Vec::new();
662 body.extend_from_slice(b"--");
663 body.extend_from_slice(BOUNDARY.as_bytes());
664 body.extend_from_slice(b"\r\n");
665 body.extend_from_slice(
666 br#"Content-Disposition: form-data; name="file"; filename="we\"ird.txt""#,
667 );
668 body.extend_from_slice(b"\r\n\r\nhello\r\n");
669 body.extend_from_slice(b"--");
670 body.extend_from_slice(BOUNDARY.as_bytes());
671 body.extend_from_slice(b"--\r\n");
672
673 let service =
674 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
675 let f = mp.next_field().await.unwrap().unwrap();
676 assert_eq!(f.file_name(), Some("we\"ird.txt"));
677 assert_eq!(f.text().await.unwrap(), "hello");
678 StatusCode::OK
679 });
680
681 let req = rama_http_types::Request::builder()
682 .method(rama_http_types::Method::POST)
683 .header(rama_http_types::header::CONTENT_TYPE, ct())
684 .body(body.into())
685 .unwrap();
686 let resp = service.serve(req).await.unwrap();
687 assert_eq!(resp.status(), StatusCode::OK);
688 }
689
690 #[tokio::test]
691 async fn test_multipart_invalid_boundary() {
692 let service = WebService::default().with_post("/", async |_: Multipart| StatusCode::OK);
693
694 let req = rama_http_types::Request::builder()
695 .method(rama_http_types::Method::POST)
696 .header(rama_http_types::header::CONTENT_TYPE, "multipart/form-data")
697 .body("ignored".into())
698 .unwrap();
699 let resp = service.serve(req).await.unwrap();
700 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
701 }
702
703 #[tokio::test]
704 async fn test_multipart_rejects_non_form_data_subtype() {
705 let service = WebService::default().with_post("/", async |_: Multipart| StatusCode::OK);
709
710 let body = body_with(&[("k", None, None, b"v")]);
711 let req = rama_http_types::Request::builder()
712 .method(rama_http_types::Method::POST)
713 .header(
714 rama_http_types::header::CONTENT_TYPE,
715 format!("multipart/mixed; boundary={BOUNDARY}"),
716 )
717 .body(body.into())
718 .unwrap();
719 let resp = service.serve(req).await.unwrap();
720 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
721 }
722
723 #[tokio::test]
724 async fn test_multipart_rejects_missing_content_type() {
725 let service = WebService::default().with_post("/", async |_: Multipart| StatusCode::OK);
726 let req = rama_http_types::Request::builder()
727 .method(rama_http_types::Method::POST)
728 .body("ignored".into())
729 .unwrap();
730 let resp = service.serve(req).await.unwrap();
731 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
732 }
733
734 #[tokio::test]
735 async fn test_multipart_field_limit_exceeded() {
736 let service = WebService::default().with_post(
737 "/",
738 async |mut mp: Multipart| -> Result<StatusCode, MultipartRejection> {
739 let f = mp.next_field().await?.unwrap();
740 _ = f.bytes().await?;
741 Ok(StatusCode::OK)
742 },
743 );
744
745 let body = body_with(&[("blob", None, None, &[42u8; 64])]);
746 let req = rama_http_types::Request::builder()
747 .method(rama_http_types::Method::POST)
748 .header(rama_http_types::header::CONTENT_TYPE, ct())
749 .body(body.into())
750 .unwrap();
751 req.extensions()
752 .insert(MultipartConfig::new().with_default_field_limit(8));
753 let resp = service.serve(req).await.unwrap();
754 assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
755 }
756
757 #[tokio::test]
758 async fn test_multipart_field_as_stream_chunks() {
759 let service =
760 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
761 let mut field = mp.next_field().await.unwrap().unwrap();
762 let mut total = Vec::new();
763 while let Some(chunk) = field.chunk().await.unwrap() {
764 total.extend_from_slice(&chunk);
765 }
766 assert_eq!(total, b"hello world");
767 StatusCode::OK
768 });
769
770 let body = body_with(&[("payload", None, None, b"hello world")]);
771 let req = rama_http_types::Request::builder()
772 .method(rama_http_types::Method::POST)
773 .header(rama_http_types::header::CONTENT_TYPE, ct())
774 .body(body.into())
775 .unwrap();
776 let resp = service.serve(req).await.unwrap();
777 assert_eq!(resp.status(), StatusCode::OK);
778 }
779
780 #[tokio::test]
781 async fn test_roundtrip_with_client_form() {
782 use crate::service::client::multipart as client;
783
784 let service =
785 WebService::default().with_post("/", async |mut mp: Multipart| -> StatusCode {
786 let f1 = mp.next_field().await.unwrap().unwrap();
787 assert_eq!(f1.name(), Some("greeting"));
788 assert_eq!(f1.text().await.unwrap(), "hello world");
789
790 let f2 = mp.next_field().await.unwrap().unwrap();
791 assert_eq!(f2.name(), Some("payload"));
792 assert_eq!(f2.file_name(), Some("blob.bin"));
793 assert_eq!(
794 f2.content_type().map(|m| m.essence_str()),
795 Some("application/octet-stream")
796 );
797 assert_eq!(f2.bytes().await.unwrap().as_ref(), &[7u8, 8, 9, 10]);
798
799 assert!(mp.next_field().await.unwrap().is_none());
800 StatusCode::OK
801 });
802
803 let form = client::Form::new().text("greeting", "hello world").part(
804 "payload",
805 client::Part::bytes([7u8, 8, 9, 10].as_slice())
806 .with_file_name("blob.bin")
807 .with_mime(crate::mime::APPLICATION_OCTET_STREAM),
808 );
809 let content_type = form.content_type();
810 let req = rama_http_types::Request::builder()
811 .method(rama_http_types::Method::POST)
812 .header(rama_http_types::header::CONTENT_TYPE, content_type)
813 .body(form.into_body())
814 .unwrap();
815 let resp = service.serve(req).await.unwrap();
816 assert_eq!(resp.status(), StatusCode::OK);
817 }
818
819 #[test]
820 fn test_config_merge_floor_keeps_min() {
821 let mut a = MultipartConfig::new()
822 .with_default_field_limit(1024)
823 .with_field_limit("avatar", kib_u64(8));
824 let b = MultipartConfig::new()
825 .with_default_field_limit(2048)
826 .with_field_limit("avatar", kib_u64(4))
827 .with_field_limit("doc", 100);
828 a.merge_floor(&b);
829 assert_eq!(a.default_field_limit, Some(1024));
830 assert_eq!(a.field_limits.get("avatar").copied(), Some(kib_u64(4)));
831 assert_eq!(a.field_limits.get("doc").copied(), Some(100));
832 }
833}