From 94afeedad60972dbe79325a0e8aed2824c57f922 Mon Sep 17 00:00:00 2001 From: seonghun lee Date: Thu, 17 Sep 2026 00:26:17 +0900 Subject: [PATCH 1/2] Enforce disk usage limit when spilling part to disk Previously, PartGenerator only enforced maxDiskUsagePerPart for body buffers that arrived after a part had switched to file storage. The content accumulated in memory that triggered the switch was written to disk without any check against maxDiskUsagePerPart. As a result, a part whose last body buffer caused the in-memory overflow was accepted in full, even when its total size exceeded the configured disk usage limit. This commit checks the accumulated byte count against maxDiskUsagePerPart before switching to file storage, and emits a DataBufferLimitException, consistent with the existing check for subsequent buffers. Closes gh-35099 Signed-off-by: seonghun lee --- .../http/codec/multipart/PartGenerator.java | 11 +++++++++++ .../DefaultPartHttpMessageReaderTests.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index fa6074616b9..7f706f22c9b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -56,6 +56,7 @@ import org.springframework.util.FastByteArrayOutputStream; * of {@link Part} objects. * * @author Arjen Poutsma + * @author Seonghun Lee * @since 5.3 */ @SuppressWarnings("NullAway") // Dataflow analysis limitation @@ -380,6 +381,16 @@ final class PartGenerator extends BaseSubscriber { } private void switchToFile(DataBuffer current, long byteCount) { + if (PartGenerator.this.maxDiskUsagePerPart != -1 && byteCount > PartGenerator.this.maxDiskUsagePerPart) { + DataBufferUtils.release(current); + this.content.forEach(DataBufferUtils::release); + this.content.clear(); + emitError(new DataBufferLimitException( + "Part exceeded the disk usage limit of " + PartGenerator.this.maxDiskUsagePerPart + + " bytes")); + return; + } + List content = new ArrayList<>(this.content); content.add(current); this.releaseOnDispose = false; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index e7b468cafa7..a7b8fadaa43 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -63,6 +63,7 @@ import static org.springframework.core.io.buffer.DataBufferUtils.release; * * @author Arjen Poutsma * @author Brian Clozel + * @author Seonghun Lee */ class DefaultPartHttpMessageReaderTests extends AbstractLeakCheckingTests { @@ -314,6 +315,20 @@ class DefaultPartHttpMessageReaderTests extends AbstractLeakCheckingTests { .verify(); } + @Test // gh-35099 + void exceedDiskUsageOnSpillOver() { + MockServerHttpRequest request = createRequest("files.multipart", "\"----WebKitFormBoundaryG8fJ50opQOML0oGD\""); + + DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader(); + reader.setMaxInMemorySize(90); + reader.setMaxDiskUsagePerPart(99); + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + StepVerifier.create(result) + .expectError(DataBufferLimitException.class) + .verify(); + } + @ParameterizedDefaultPartHttpMessageReaderTest void emptyLastPart(DefaultPartHttpMessageReader reader) throws InterruptedException { MockServerHttpRequest request = createRequest( From 8107a2b5617199fcb0b4522ba075eda932c59695 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 17 Sep 2026 16:47:26 +0200 Subject: [PATCH 2/2] Polishing contribution See gh-37285 --- .../springframework/http/codec/multipart/PartGenerator.java | 3 +-- .../codec/multipart/DefaultPartHttpMessageReaderTests.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 7f706f22c9b..d3bc65d9232 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -383,8 +383,7 @@ final class PartGenerator extends BaseSubscriber { private void switchToFile(DataBuffer current, long byteCount) { if (PartGenerator.this.maxDiskUsagePerPart != -1 && byteCount > PartGenerator.this.maxDiskUsagePerPart) { DataBufferUtils.release(current); - this.content.forEach(DataBufferUtils::release); - this.content.clear(); + this.dispose(); emitError(new DataBufferLimitException( "Part exceeded the disk usage limit of " + PartGenerator.this.maxDiskUsagePerPart + " bytes")); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index a7b8fadaa43..369fe6a61ef 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -317,7 +317,7 @@ class DefaultPartHttpMessageReaderTests extends AbstractLeakCheckingTests { @Test // gh-35099 void exceedDiskUsageOnSpillOver() { - MockServerHttpRequest request = createRequest("files.multipart", "\"----WebKitFormBoundaryG8fJ50opQOML0oGD\""); + MockServerHttpRequest request = createRequest(new ClassPathResource("files.multipart", getClass()), "\"----WebKitFormBoundaryG8fJ50opQOML0oGD\""); DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader(); reader.setMaxInMemorySize(90);