1515
1616use std:: {
1717 borrow:: Cow ,
18+ cell:: RefCell ,
1819 fmt,
1920 sync:: atomic:: { AtomicUsize , Ordering } ,
2021} ;
@@ -81,6 +82,18 @@ pub enum TypedMultipartError {
8182 /// The configured limit in bytes.
8283 limit_bytes : usize ,
8384 } ,
85+ /// The cumulative bytes read across all fields exceeded the request cap.
86+ RequestTooLarge {
87+ /// Name of the field whose chunk crossed the aggregate cap.
88+ field_name : String ,
89+ /// The configured aggregate limit in bytes.
90+ limit_bytes : usize ,
91+ } ,
92+ /// The multipart request contained more parts than the configured cap.
93+ TooManyFields {
94+ /// The configured maximum number of fields.
95+ limit_fields : usize ,
96+ } ,
8497 /// A catch-all for other errors during multipart processing.
8598 Other {
8699 /// Description of the error.
@@ -129,6 +142,19 @@ impl fmt::Display for TypedMultipartError {
129142 "Field `{field_name}` exceeds size limit of {limit_bytes} bytes"
130143 )
131144 }
145+ Self :: RequestTooLarge {
146+ field_name,
147+ limit_bytes,
148+ } => write ! (
149+ f,
150+ "Multipart request exceeds aggregate size limit of {limit_bytes} bytes while reading field `{field_name}`"
151+ ) ,
152+ Self :: TooManyFields { limit_fields } => {
153+ write ! (
154+ f,
155+ "Multipart request exceeds field count limit of {limit_fields}"
156+ )
157+ }
132158 Self :: Other { source } => write ! ( f, "{source}" ) ,
133159 }
134160 }
@@ -146,6 +172,8 @@ impl std::error::Error for TypedMultipartError {
146172 | Self :: InvalidEnumValue { .. }
147173 | Self :: NamelessField
148174 | Self :: FieldTooLarge { .. }
175+ | Self :: RequestTooLarge { .. }
176+ | Self :: TooManyFields { .. }
149177 | Self :: Other { .. } => None ,
150178 }
151179 }
@@ -161,10 +189,12 @@ impl TypedMultipartError {
161189 | Self :: DuplicateField { field_name }
162190 | Self :: UnknownField { field_name }
163191 | Self :: InvalidEnumValue { field_name, .. }
164- | Self :: FieldTooLarge { field_name, .. } => Some ( field_name) ,
192+ | Self :: FieldTooLarge { field_name, .. }
193+ | Self :: RequestTooLarge { field_name, .. } => Some ( field_name) ,
165194 Self :: InvalidRequest { .. }
166195 | Self :: InvalidRequestBody { .. }
167196 | Self :: NamelessField
197+ | Self :: TooManyFields { .. }
168198 | Self :: Other { .. } => None ,
169199 }
170200 }
@@ -250,7 +280,9 @@ impl IntoResponse for TypedMultipartError {
250280 // unsupported multipart media type. Keep this aligned with
251281 // `Validated<T>`'s validation-failure status.
252282 Self :: WrongFieldType { .. } => StatusCode :: UNPROCESSABLE_ENTITY ,
253- Self :: FieldTooLarge { .. } => StatusCode :: PAYLOAD_TOO_LARGE ,
283+ Self :: FieldTooLarge { .. }
284+ | Self :: RequestTooLarge { .. }
285+ | Self :: TooManyFields { .. } => StatusCode :: PAYLOAD_TOO_LARGE ,
254286 Self :: Other { .. } => StatusCode :: INTERNAL_SERVER_ERROR ,
255287 } ;
256288 // Serialize the canonical 422 envelope (see `error_body` /
@@ -441,6 +473,150 @@ impl<T> std::ops::DerefMut for TypedMultipart<T> {
441473 }
442474}
443475
476+ /// Default aggregate cap for a typed multipart request body.
477+ ///
478+ /// This cap is intentionally much higher than axum's built-in
479+ /// [`DefaultBodyLimit`](axum::extract::DefaultBodyLimit) default because the
480+ /// two policies guard different layers: axum may reject the raw HTTP body before
481+ /// Vespera sees it, while this cap still applies when applications explicitly
482+ /// disable or raise axum's body limit for in-process/JNI uploads.
483+ pub const DEFAULT_MULTIPART_MAX_TOTAL_BYTES : usize = 512 * 1024 * 1024 ; // 512 MiB
484+
485+ /// Default maximum number of parts in a typed multipart request.
486+ pub const DEFAULT_MULTIPART_MAX_FIELDS : usize = 1024 ;
487+
488+ static DEFAULT_MULTIPART_TOTAL_LIMIT : AtomicUsize =
489+ AtomicUsize :: new ( DEFAULT_MULTIPART_MAX_TOTAL_BYTES ) ;
490+ static DEFAULT_MULTIPART_FIELD_LIMIT : AtomicUsize = AtomicUsize :: new ( DEFAULT_MULTIPART_MAX_FIELDS ) ;
491+
492+ /// Aggregate resource policy for [`TypedMultipart`].
493+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
494+ pub struct MultipartLimits {
495+ /// Maximum cumulative bytes accepted across all parsed fields.
496+ pub max_total_bytes : usize ,
497+ /// Maximum number of parsed fields accepted in one request.
498+ pub max_fields : usize ,
499+ }
500+
501+ impl MultipartLimits {
502+ /// Construct an aggregate multipart policy.
503+ #[ must_use]
504+ pub const fn new ( max_total_bytes : usize , max_fields : usize ) -> Self {
505+ Self {
506+ max_total_bytes,
507+ max_fields,
508+ }
509+ }
510+ }
511+
512+ /// Return the process-wide default aggregate multipart policy.
513+ #[ must_use]
514+ pub fn default_multipart_limits ( ) -> MultipartLimits {
515+ MultipartLimits :: new (
516+ DEFAULT_MULTIPART_TOTAL_LIMIT . load ( Ordering :: Relaxed ) ,
517+ DEFAULT_MULTIPART_FIELD_LIMIT . load ( Ordering :: Relaxed ) ,
518+ )
519+ }
520+
521+ /// Set the process-wide default aggregate multipart policy.
522+ ///
523+ /// Prefer calling this during application startup, before request handling. For
524+ /// per-route policies use [`TypedMultipartWithLimits`], which avoids global
525+ /// process state and is therefore safer in tests and multi-tenant apps.
526+ pub fn set_default_multipart_limits ( limits : MultipartLimits ) -> MultipartLimits {
527+ MultipartLimits :: new (
528+ DEFAULT_MULTIPART_TOTAL_LIMIT . swap ( limits. max_total_bytes , Ordering :: Relaxed ) ,
529+ DEFAULT_MULTIPART_FIELD_LIMIT . swap ( limits. max_fields , Ordering :: Relaxed ) ,
530+ )
531+ }
532+
533+ #[ derive( Debug ) ]
534+ struct MultipartAggregateState {
535+ limits : MultipartLimits ,
536+ total_bytes : usize ,
537+ fields : usize ,
538+ }
539+
540+ impl MultipartAggregateState {
541+ const fn new ( limits : MultipartLimits ) -> Self {
542+ Self {
543+ limits,
544+ total_bytes : 0 ,
545+ fields : 0 ,
546+ }
547+ }
548+ }
549+
550+ tokio:: task_local! {
551+ static MULTIPART_AGGREGATE : RefCell <MultipartAggregateState >;
552+ }
553+
554+ fn register_multipart_field ( ) -> Result < ( ) , TypedMultipartError > {
555+ MULTIPART_AGGREGATE
556+ . try_with ( |state| {
557+ let mut state = state. borrow_mut ( ) ;
558+ state. fields = state. fields . saturating_add ( 1 ) ;
559+ if state. fields > state. limits . max_fields {
560+ return Err ( TypedMultipartError :: TooManyFields {
561+ limit_fields : state. limits . max_fields ,
562+ } ) ;
563+ }
564+ Ok ( ( ) )
565+ } )
566+ // Field parsers can be unit-tested outside the extractor. In that shape
567+ // there is no request aggregate to update, so per-field limits remain the
568+ // only active guard instead of failing spuriously.
569+ . unwrap_or ( Ok ( ( ) ) )
570+ }
571+
572+ fn register_multipart_bytes ( field_name : & str , chunk_len : usize ) -> Result < ( ) , TypedMultipartError > {
573+ MULTIPART_AGGREGATE
574+ . try_with ( |state| {
575+ let mut state = state. borrow_mut ( ) ;
576+ state. total_bytes = state. total_bytes . saturating_add ( chunk_len) ;
577+ if state. total_bytes > state. limits . max_total_bytes {
578+ return Err ( TypedMultipartError :: RequestTooLarge {
579+ field_name : field_name. to_owned ( ) ,
580+ limit_bytes : state. limits . max_total_bytes ,
581+ } ) ;
582+ }
583+ Ok ( ( ) )
584+ } )
585+ . unwrap_or ( Ok ( ( ) ) )
586+ }
587+
588+ /// Axum extractor variant with const aggregate multipart limits.
589+ ///
590+ /// Use this when a route needs a tighter or looser request-level policy than
591+ /// the process default. Per-field `#[form_data(limit = "...")]` caps still
592+ /// apply independently: the effective policy is whichever per-field or
593+ /// aggregate limit is exceeded first.
594+ pub struct TypedMultipartWithLimits <
595+ T ,
596+ const MAX_TOTAL_BYTES : usize ,
597+ const MAX_FIELDS : usize = DEFAULT_MULTIPART_MAX_FIELDS ,
598+ > ( pub T ) ;
599+
600+ async fn parse_typed_multipart_with_limits < T , S > (
601+ req : Request ,
602+ state : & S ,
603+ limits : MultipartLimits ,
604+ ) -> Result < T , TypedMultipartError >
605+ where
606+ T : TryFromMultipartWithState < S > ,
607+ S : Send + Sync + ' static ,
608+ {
609+ let mut multipart = axum:: extract:: Multipart :: from_request ( req, state)
610+ . await
611+ . map_err ( TypedMultipartError :: from) ?;
612+ MULTIPART_AGGREGATE
613+ . scope (
614+ RefCell :: new ( MultipartAggregateState :: new ( limits) ) ,
615+ async move { T :: try_from_multipart_with_state ( & mut multipart, state) . await } ,
616+ )
617+ . await
618+ }
619+
444620impl < T , S > FromRequest < S > for TypedMultipart < T >
445621where
446622 T : TryFromMultipartWithState < S > ,
@@ -449,10 +625,27 @@ where
449625 type Rejection = TypedMultipartError ;
450626
451627 async fn from_request ( req : Request , state : & S ) -> Result < Self , Self :: Rejection > {
452- let mut multipart = axum:: extract:: Multipart :: from_request ( req, state)
453- . await
454- . map_err ( TypedMultipartError :: from) ?;
455- let value = T :: try_from_multipart_with_state ( & mut multipart, state) . await ?;
628+ let value =
629+ parse_typed_multipart_with_limits ( req, state, default_multipart_limits ( ) ) . await ?;
630+ Ok ( Self ( value) )
631+ }
632+ }
633+
634+ impl < T , S , const MAX_TOTAL_BYTES : usize , const MAX_FIELDS : usize > FromRequest < S >
635+ for TypedMultipartWithLimits < T , MAX_TOTAL_BYTES , MAX_FIELDS >
636+ where
637+ T : TryFromMultipartWithState < S > ,
638+ S : Send + Sync + ' static ,
639+ {
640+ type Rejection = TypedMultipartError ;
641+
642+ async fn from_request ( req : Request , state : & S ) -> Result < Self , Self :: Rejection > {
643+ let value = parse_typed_multipart_with_limits (
644+ req,
645+ state,
646+ MultipartLimits :: new ( MAX_TOTAL_BYTES , MAX_FIELDS ) ,
647+ )
648+ . await ?;
456649 Ok ( Self ( value) )
457650 }
458651}
@@ -483,11 +676,13 @@ async fn read_field_data(
483676 limit : Option < usize > ,
484677 initial_capacity : usize ,
485678) -> Result < ( Field < ' _ > , Vec < u8 > ) , TypedMultipartError > {
679+ register_multipart_field ( ) ?;
486680 // Initial capacity is independent from the hard byte limit: tiny scalar
487681 // fields keep the 256B cap without preallocating 256B per bool/number.
488682 let capacity = limit. map_or ( initial_capacity, |limit| initial_capacity. min ( limit) ) ;
489683 let mut buf = Vec :: with_capacity ( capacity) ;
490684 while let Some ( chunk) = field. chunk ( ) . await ? {
685+ register_multipart_bytes ( field. name ( ) . unwrap_or_default ( ) , chunk. len ( ) ) ?;
491686 if let Some ( limit) = limit
492687 && buf. len ( ) . saturating_add ( chunk. len ( ) ) > limit
493688 {
@@ -709,6 +904,7 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
709904 limit_bytes : Option < usize > ,
710905 _state : & S ,
711906 ) -> Result < Self , TypedMultipartError > {
907+ register_multipart_field ( ) ?;
712908 // Temp-file creation AND reopen() are both blocking syscalls —
713909 // run them together on the blocking pool so neither stalls the
714910 // async worker (the reopen previously ran inline on the async
@@ -734,6 +930,7 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
734930 let limit_bytes = limit_bytes. unwrap_or_else ( default_temp_file_field_limit_bytes) ;
735931 let mut total = 0usize ;
736932 while let Some ( chunk) = field. chunk ( ) . await ? {
933+ register_multipart_bytes ( field. name ( ) . unwrap_or_default ( ) , chunk. len ( ) ) ?;
737934 // `saturating_add` (matching `read_field_data`) prevents a
738935 // pathological chunk size from wrapping `total` and slipping
739936 // past the limit check below.
0 commit comments