Repository: helidon-io/helidon (2026-03-30T10:57:17Z)

Issue:
ReadableEntityBase.read with zero length may request more data
When `io.helidon.http.media.ReadableEntityBase.RequestingInputStream#read(byte[], int, int)` is invoked with length 0, we may try to read more data, even though it we are not supposed to


Discussion:

Patch:
diff --git a/http/media/media/src/main/java/io/helidon/http/media/ReadableEntityBase.java b/http/media/media/src/main/java/io/helidon/http/media/ReadableEntityBase.java
index cadcb40d6df..b9bd84f5cc3 100644
--- a/http/media/media/src/main/java/io/helidon/http/media/ReadableEntityBase.java
+++ b/http/media/media/src/main/java/io/helidon/http/media/ReadableEntityBase.java
@@ -21,6 +21,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UncheckedIOException;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -301,6 +302,10 @@ public void close() throws IOException {
 
         @Override
         public int read(byte[] b, int off, int len) {
+            Objects.checkFromIndexSize(off, len, b.length);
+            if (len == 0) {
+                return 0;
+            }
             if (finished) {
                 return -1;
             }


Test patch:
diff --git a/http/media/media/src/test/java/io/helidon/http/media/ReadableEntityBaseTest.java b/http/media/media/src/test/java/io/helidon/http/media/ReadableEntityBaseTest.java
index b74ca6cf99b..36c4e7bc395 100644
--- a/http/media/media/src/test/java/io/helidon/http/media/ReadableEntityBaseTest.java
+++ b/http/media/media/src/test/java/io/helidon/http/media/ReadableEntityBaseTest.java
@@ -18,7 +18,9 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 
 import io.helidon.common.GenericType;
@@ -93,6 +95,34 @@ void testMaxBufferedEntityLength() {
         assertThat(e2.getMessage(), is("Entity has already been requested. Entity cannot be requested multiple times"));
     }
 
+    @Test
+    void testZeroLengthReadDoesNotRequestMoreData() throws IOException {
+        byte[] payload = "ping".getBytes(StandardCharsets.UTF_8);
+        AtomicInteger reads = new AtomicInteger();
+        ReadableEntityBase entityBase = new ReadableEntityImpl(estimate -> {
+            if (reads.getAndIncrement() == 0) {
+                return BufferData.create(payload);
+            }
+            return BufferData.empty();
+        }, 1024);
+
+        InputStream inputStream = entityBase.inputStream();
+        assertThat(Arrays.equals(inputStream.readNBytes(payload.length), payload), is(true));
+        assertThat(inputStream.read(new byte[0]), is(0));
+        assertThat(reads.get(), is(1));
+        inputStream.close();
+    }
+
+    @Test
+    void testZeroLengthReadStillValidatesArguments() throws IOException {
+        try (InputStream inputStream = new ReadableEntityImpl(new Readable(), 1024).inputStream()) {
+            assertThrows(NullPointerException.class, () -> inputStream.read(null, 0, 0));
+        }
+        try (InputStream inputStream = new ReadableEntityImpl(new Readable(), 1024).inputStream()) {
+            assertThrows(IndexOutOfBoundsException.class, () -> inputStream.read(new byte[1], 2, 0));
+        }
+    }
+
     static class Readable implements Function<Integer, BufferData> {
         private boolean done;