From ef4ffa0005a7e33946559a5b475edca4f975de64 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 6 Dec 2023 11:51:48 +0100 Subject: [PATCH] Support empty part in DefaultPartHttpMessageReader This commit fixes a bug in DefaultPartHttpMessageReader's MultipartParser, due to which the last token in a part window was not properly indicated. Closes gh-30953 --- .../http/codec/multipart/MultipartParser.java | 2 +- .../DefaultPartHttpMessageReaderTests.java | 17 +++++++++++++++++ .../http/codec/multipart/empty-part.multipart | 13 +++++++++++++ ...MultipartRouterFunctionIntegrationTests.java | 8 +++++++- 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart 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 0e6086221f4..e0097c03674 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 @@ -540,7 +540,7 @@ final class MultipartParser extends BaseSubscriber { while ((prev = this.queue.pollLast()) != null) { int prevByteCount = prev.readableByteCount(); int prevLen = prevByteCount + len; - if (prevLen > 0) { + if (prevLen >= 0) { // slice body part of previous buffer, and flush it DataBuffer body = prev.split(prevLen + prev.readPosition()); DataBufferUtils.release(prev); 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 64efd900a92..24c4af12f75 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 @@ -301,6 +301,23 @@ class DefaultPartHttpMessageReaderTests { latch.await(); } + @ParameterizedDefaultPartHttpMessageReaderTest + void emptyLastPart(DefaultPartHttpMessageReader reader) throws InterruptedException { + MockServerHttpRequest request = createRequest( + new ClassPathResource("empty-part.multipart", getClass()), "LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD"); + + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + CountDownLatch latch = new CountDownLatch(2); + StepVerifier.create(result) + .consumeNextWith(part -> testPart(part, null, "", latch)) + .consumeNextWith(part -> testPart(part, null, "", latch)) + .verifyComplete(); + + latch.await(); + } + + private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary) throws InterruptedException { diff --git a/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart b/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart new file mode 100644 index 00000000000..501388b7819 --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.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-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index f91cbf71f17..f59892ca303 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -257,13 +257,19 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte assertThat(data).hasSize(2); List fileData = data.get(0); - assertThat(fileData).hasSize(1); + assertThat(fileData).hasSize(2); assertThat(fileData).element(0).isInstanceOf(FilePartEvent.class); FilePartEvent filePartEvent = (FilePartEvent) fileData.get(0); assertThat(filePartEvent.name()).isEqualTo("fooPart"); assertThat(filePartEvent.filename()).isEqualTo("foo.txt"); DataBufferUtils.release(filePartEvent.content()); + assertThat(fileData).element(1).isInstanceOf(FilePartEvent.class); + filePartEvent = (FilePartEvent) fileData.get(1); + assertThat(filePartEvent.name()).isEqualTo("fooPart"); + assertThat(filePartEvent.filename()).isEqualTo("foo.txt"); + DataBufferUtils.release(filePartEvent.content()); + List fieldData = data.get(1); assertThat(fieldData).hasSize(1); assertThat(fieldData).element(0).isInstanceOf(FormPartEvent.class);