@@ -310,8 +310,9 @@ impl IntoResponse for TypedMultipartError {
310310 | Self :: TooManyFields { .. } => StatusCode :: PAYLOAD_TOO_LARGE ,
311311 Self :: Other { .. } => StatusCode :: INTERNAL_SERVER_ERROR ,
312312 } ;
313- // Serialize the canonical 422 envelope (see `error_body` /
314- // module-scope `MultipartErrorEnvelope`).
313+ // Serialize the canonical JSON error envelope (see `error_body` /
314+ // module-scope `MultipartErrorEnvelope`); the status varies (400/413/
315+ // 422/500) but the body shape is identical.
315316 let body = self . error_body ( ) ;
316317 (
317318 status,
@@ -580,7 +581,16 @@ tokio::task_local! {
580581 static MULTIPART_AGGREGATE : RefCell <MultipartAggregateState >;
581582}
582583
583- fn register_multipart_field ( ) -> Result < ( ) , TypedMultipartError > {
584+ /// Count one multipart PART against the request-wide `max_fields` limit.
585+ ///
586+ /// Invoked by the derived `TryFromMultipart` loop **once per wire part** —
587+ /// before the field name is resolved — so EVERY part (known, unknown, or
588+ /// nameless) is counted exactly once. Counting inside the per-known-field
589+ /// parsers instead let unknown parts in non-strict mode (the `_ => {}`
590+ /// dispatch arm) slip past the cap entirely, so a request with thousands of
591+ /// unknown parts could burn unbounded parser/boundary-scan work without ever
592+ /// tripping `TooManyFields`.
593+ pub fn register_multipart_part ( ) -> Result < ( ) , TypedMultipartError > {
584594 MULTIPART_AGGREGATE
585595 . try_with ( |state| {
586596 let mut state = state. borrow_mut ( ) ;
@@ -592,9 +602,8 @@ fn register_multipart_field() -> Result<(), TypedMultipartError> {
592602 }
593603 Ok ( ( ) )
594604 } )
595- // Field parsers can be unit-tested outside the extractor. In that shape
596- // there is no request aggregate to update, so per-field limits remain the
597- // only active guard instead of failing spuriously.
605+ // The derived impl can be unit-tested outside the extractor scope; with
606+ // no request aggregate present, counting no-ops rather than failing.
598607 . unwrap_or ( Ok ( ( ) ) )
599608}
600609
@@ -705,7 +714,8 @@ async fn read_field_data(
705714 limit : Option < usize > ,
706715 initial_capacity : usize ,
707716) -> Result < ( Field < ' _ > , Vec < u8 > ) , TypedMultipartError > {
708- register_multipart_field ( ) ?;
717+ // Part counting now happens once per part in the derived loop
718+ // (`register_multipart_part`), so the field parsers no longer count.
709719 // Initial capacity is independent from the hard byte limit: tiny scalar
710720 // fields keep the 256B cap without preallocating 256B per bool/number.
711721 let capacity = limit. map_or ( initial_capacity, |limit| initial_capacity. min ( limit) ) ;
@@ -940,7 +950,8 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
940950 limit_bytes : Option < usize > ,
941951 _state : & S ,
942952 ) -> Result < Self , TypedMultipartError > {
943- register_multipart_field ( ) ?;
953+ // Part counting happens once per part in the derived loop
954+ // (`register_multipart_part`); the temp-file parser no longer counts.
944955 // Temp-file creation AND reopen() are both blocking syscalls —
945956 // run them together on the blocking pool so neither stalls the
946957 // async worker (the reopen previously ran inline on the async
0 commit comments