From d550ab131386991dcb22072c154c91b232eb8013 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 4 Sep 2026 09:24:15 +0200 Subject: [PATCH] Count in memory buffered data against limit in PartGeenrator The new PartGenerator for parsing multipart requests supports buffering the content in memory and switching to a file after a configured size. More specifically, when a multipart part exceeds maxInMemorySize and InMemoryState switches it over to FileState, the bytes that were already buffered in memory are flushed to the temp file. Prior to this commit, this was done via FileState.writeBuffer(), meaning that the in memory buffered data would not be counted against the configured limit for writing to a file. This commit fixes this by writing buffered data with FileState.onBody(). Fixes gh-37238 --- .../http/converter/multipart/PartGenerator.java | 2 +- .../multipart/MultipartHttpMessageConverterTests.java | 11 +++++++++++ .../http/multipart/large-file.multipart | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/test/resources/org/springframework/http/multipart/large-file.multipart diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/PartGenerator.java index 5e1046e13ae..34693f64071 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/PartGenerator.java @@ -293,7 +293,7 @@ final class PartGenerator implements MultipartParser.PartListener { private void switchToFile(DataBuffer current, boolean last) { FileState fileState = new FileState(this.headers, PartGenerator.this.fileStorageDirectory); - this.content.forEach(fileState::writeBuffer); + this.content.forEach(buffer -> fileState.onBody(buffer, false)); fileState.onBody(current, last); PartGenerator.this.state = fileState; } diff --git a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java index 14200fd12ae..edffedf97c4 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverterTests.java @@ -236,6 +236,17 @@ class MultipartHttpMessageConverterTests { .isInstanceOf(HttpMessageConversionException.class).hasMessage("Part exceeded the disk usage limit of 35 bytes"); } + @Test // gh-37238 + void readMultipartMaxDiskUsageExceededAcrossBufferedPrefix() throws Exception { + MockHttpInputMessage response = createMultipartResponse("large-file.multipart", "B"); + converter.setMaxInMemorySize(1935); + // just over the max in memory, will only fail if buffered memory is counted for when writing to file + converter.setMaxDiskUsagePerPart(1945); + assertThatThrownBy(() -> converter.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class), response, null)) + .isInstanceOf(HttpMessageConversionException.class) + .hasMessage("Part exceeded the disk usage limit of 1945 bytes"); + } + @Test void readMultipartUnnamedPart() throws Exception { MockHttpInputMessage response = createMultipartResponse("simple.multipart", "simple-boundary"); diff --git a/spring-web/src/test/resources/org/springframework/http/multipart/large-file.multipart b/spring-web/src/test/resources/org/springframework/http/multipart/large-file.multipart new file mode 100644 index 00000000000..deb03115c67 --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/multipart/large-file.multipart @@ -0,0 +1,6 @@ +--B +Content-Disposition: form-data; name="file"; filename="test.txt" +Content-Type: application/octet-stream + +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +--B--