diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index 24df791d90f..879b05c41a5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -546,6 +546,7 @@ final class MultipartParser extends BaseSubscriber { // iterate over buffers in reverse order DataBufferUtils.release(boundaryBuffer); DataBuffer prev; + boolean found = false; while ((prev = this.queue.pollLast()) != null) { int prevByteCount = prev.readableByteCount(); int prevLen = prevByteCount + len; @@ -555,6 +556,7 @@ final class MultipartParser extends BaseSubscriber { DataBufferUtils.release(prev); enqueue(body); flush(); + found = true; break; } else { @@ -563,11 +565,21 @@ final class MultipartParser extends BaseSubscriber { len += prevByteCount; } } + if (!found) { + // all buffered bytes were boundary bytes: the part had an empty body + emitBody(buffer.factory().allocateBuffer(0), true); + } } else /* if (len == 0) */ { // buffer starts with complete delimiter, flush out the previous buffers DataBufferUtils.release(boundaryBuffer); - flush(); + if (this.queue.isEmpty()) { + // nothing was ever buffered for this part: the part had an empty body + emitBody(buffer.factory().allocateBuffer(0), true); + } + else { + flush(); + } } changeState(this, new HeadersState(), buffer); 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 2464480ca62..372f1481b00 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 @@ -316,7 +316,7 @@ class DefaultPartHttpMessageReaderTests extends AbstractLeakCheckingTests { @ParameterizedDefaultPartHttpMessageReaderTest void emptyLastPart(DefaultPartHttpMessageReader reader) throws InterruptedException { MockServerHttpRequest request = createRequest( - "empty-part.multipart", "LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD"); + "empty-part-last.multipart", "LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD"); Flux result = reader.read(forClass(Part.class), request, emptyMap()); @@ -329,6 +329,22 @@ class DefaultPartHttpMessageReaderTests extends AbstractLeakCheckingTests { latch.await(); } + @ParameterizedDefaultPartHttpMessageReaderTest // gh-37264 + void emptyPartNotLast(DefaultPartHttpMessageReader reader) throws InterruptedException { + MockServerHttpRequest request = createRequest( + "empty-part.multipart", "simple-boundary"); + + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + CountDownLatch latch = new CountDownLatch(2); + StepVerifier.create(result) + .consumeNextWith(part -> testPart(part, "file", "", latch)).as("file") + .consumeNextWith(part -> testPart(part, "action", "asd", latch)).as("action") + .verifyComplete(); + + latch.await(); + } + private void testBrowser(DefaultPartHttpMessageReader reader, String fileName, String boundary) throws InterruptedException { diff --git a/spring-web/src/test/resources/org/springframework/http/multipart/empty-part-last.multipart b/spring-web/src/test/resources/org/springframework/http/multipart/empty-part-last.multipart new file mode 100644 index 00000000000..501388b7819 --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/multipart/empty-part-last.multipart @@ -0,0 +1,13 @@ +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD +Content-Disposition: form-data; name="files"; filename="file17312898095703516893.tmp" +Content-Type: application/octet-stream +Content-Length: 0 + + +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD +Content-Disposition: form-data; name="files"; filename="file14790463448453253614.tmp" +Content-Type: application/octet-stream +Content-Length: 0 + + +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD-- diff --git a/spring-web/src/test/resources/org/springframework/http/multipart/empty-part.multipart b/spring-web/src/test/resources/org/springframework/http/multipart/empty-part.multipart index 501388b7819..6613946f0d8 100644 --- a/spring-web/src/test/resources/org/springframework/http/multipart/empty-part.multipart +++ b/spring-web/src/test/resources/org/springframework/http/multipart/empty-part.multipart @@ -1,13 +1,9 @@ ---LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD -Content-Disposition: form-data; name="files"; filename="file17312898095703516893.tmp" -Content-Type: application/octet-stream -Content-Length: 0 +--simple-boundary +Content-Disposition: form-data; name="file"; filename="test.txt" ---LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD -Content-Disposition: form-data; name="files"; filename="file14790463448453253614.tmp" -Content-Type: application/octet-stream -Content-Length: 0 +--simple-boundary +Content-Disposition: form-data; name="action" - ---LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD-- +asd +--simple-boundary--