From d8bc54d2e7d09d115dfb78381fbbac1ac0c90ab0 Mon Sep 17 00:00:00 2001 From: KimDaehyeon Date: Tue, 28 Apr 2026 03:55:21 +0900 Subject: [PATCH] Fix data loss in DataBufferUtils synchronous write Prior to this commit, WritableByteChannelSubscriber.hookOnNext() called iterator.next() exactly once. If a DataBuffer consisted of multiple NIO ByteBuffers (e.g., NettyDataBuffer wrapping a CompositeByteBuf), only the first buffer was written to the channel, and the remaining buffers were silently ignored and lost. This commit adds the missing while (iterator.hasNext()) outer loop to ensure all fragmented buffers exposed by the iterator are completely and safely written to the synchronous channel. See gh-36714 Signed-off-by: KimDaehyeon --- .../core/io/buffer/DataBufferUtils.java | 8 ++++--- .../core/io/buffer/DataBufferUtilsTests.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 6c12d4ed3d8..be9f6f9cc75 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -1138,9 +1138,11 @@ public abstract class DataBufferUtils { protected void hookOnNext(DataBuffer dataBuffer) { try { try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - ByteBuffer byteBuffer = iterator.next(); - while (byteBuffer.hasRemaining()) { - this.channel.write(byteBuffer); + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + while (byteBuffer.hasRemaining()) { + this.channel.write(byteBuffer); + } } } this.sink.next(dataBuffer); diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java index 00d95af03a1..35ebdd9c3ae 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -338,6 +338,27 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests { channel.close(); } + @ParameterizedDataBufferAllocatingTest + void writeWritableByteChannelWithJoinedBuffer(DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer joined = bufferFactory.join(List.of(foo, bar)); + + WritableByteChannel channel = Files.newByteChannel(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(Flux.just(joined), channel); + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foobar")) + .verifyComplete(); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foobar"); + channel.close(); + } + @ParameterizedDataBufferAllocatingTest void writeWritableByteChannelErrorInFlux(DataBufferFactory bufferFactory) throws Exception { super.bufferFactory = bufferFactory;