From 4434a494eef7706aa3fbd82fe77e1be92e943f2e Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 00:57:50 +0900 Subject: [PATCH] =?UTF-8?q?fix(multipart):=20count=20ignored=20fields=20to?= =?UTF-8?q?wards=20`MultipartFormConfig`=20li=E2=80=A6=20(#4026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(multipart): count ignored fields towards `MultipartFormConfig` limits --- actix-multipart-derive/src/lib.rs | 4 +-- actix-multipart/src/form/mod.rs | 44 +++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs index 5b5e28254..161a9ba24 100644 --- a/actix-multipart-derive/src/lib.rs +++ b/actix-multipart-derive/src/lib.rs @@ -227,7 +227,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS ::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string()) )) } else { - quote!(::std::result::Result::Ok(())) + quote!(::actix_multipart::form::discard_field(field, limits).await) }; // Value for duplicate action @@ -289,7 +289,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS ) -> ::std::pin::Pin<::std::boxed::Box> + 't>> { match field.name().unwrap() { #handle_field_impl - _ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)), + _ => return ::std::boxed::Box::pin(async move { #unknown_field_result }), } } diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs index cb89b7cc1..de0eeecaa 100644 --- a/actix-multipart/src/form/mod.rs +++ b/actix-multipart/src/form/mod.rs @@ -82,7 +82,9 @@ where ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { - DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + DuplicateField::Ignore => { + return Box::pin(async move { discard_field(field, limits).await }); + } DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( @@ -159,7 +161,9 @@ where ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { - DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + DuplicateField::Ignore => { + return Box::pin(async move { discard_field(field, limits).await }); + } DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( @@ -312,6 +316,16 @@ impl Limits { } } +/// Drain a field that will not be retained while still accounting for form limits. +#[doc(hidden)] +pub async fn discard_field(mut field: Field, limits: &mut Limits) -> Result<(), MultipartError> { + while let Some(chunk) = field.try_next().await? { + limits.try_consume_limits(chunk.len(), false)?; + } + + Ok(()) +} + /// Typed `multipart/form-data` extractor. /// /// To extract typed data from a multipart stream, the inner type `T` must implement the @@ -710,6 +724,32 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[actix_rt::test] + async fn test_discarded_fields_count_towards_total_limit() { + let srv = actix_test::start(|| { + App::new() + .route("/unknown", web::post().to(test_upload_limits_memory)) + .route("/duplicate", web::post().to(test_duplicate_ignore_route)) + .app_data( + MultipartFormConfig::default() + .memory_limit(usize::MAX) + .total_limit(20), + ) + }); + + let mut form = multipart::Form::default(); + form.add_text("field", "7 bytes"); + form.add_text("unknown", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/unknown").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let mut form = multipart::Form::default(); + form.add_text("field", "first_value"); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/duplicate").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + /// Test the Limits. #[derive(MultipartForm)] struct TestMemoryUploadLimits {