This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/11.0.x by this push:
     new 3f7bf4a890 Fix truncated byte range responses in DefaultServlet
3f7bf4a890 is described below

commit 3f7bf4a890a331322228a72d9396ab510baede43
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Oct 2 18:21:07 2026 +0100

    Fix truncated byte range responses in DefaultServlet
    
    Based on #1067 by aoto-tech
---
 .../apache/catalina/servlets/DefaultServlet.java   |  23 +--
 .../catalina/servlets/TestDefaultServlet.java      | 168 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   6 +
 3 files changed, 186 insertions(+), 11 deletions(-)

diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java 
b/java/org/apache/catalina/servlets/DefaultServlet.java
index a9bea525cb..4e9b38b7bc 100644
--- a/java/org/apache/catalina/servlets/DefaultServlet.java
+++ b/java/org/apache/catalina/servlets/DefaultServlet.java
@@ -19,6 +19,7 @@ package org.apache.catalina.servlets;
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -2846,23 +2847,23 @@ public class DefaultServlet extends HttpServlet {
         }
 
         IOException exception = null;
-        long bytesToRead = end - start + 1;
+        long rangeLength = end - start + 1;
+        long bytesToRead = rangeLength;
 
         byte[] buffer = new byte[input];
-        int len = buffer.length;
-        while ((bytesToRead > 0) && (len >= buffer.length)) {
+        while (bytesToRead > 0) {
             try {
-                len = istream.read(buffer);
-                if (bytesToRead >= len) {
-                    ostream.write(buffer, 0, len);
-                    bytesToRead -= len;
-                } else {
-                    ostream.write(buffer, 0, (int) bytesToRead);
-                    bytesToRead = 0;
+                int len = istream.read(buffer, 0, (int) 
Math.min(buffer.length, bytesToRead));
+                if (len == -1) {
+                    exception = new 
EOFException(sm.getString("defaultServlet.wrongByteCountForRange",
+                            Long.valueOf(rangeLength - bytesToRead), 
Long.valueOf(rangeLength)));
+                    break;
                 }
+                ostream.write(buffer, 0, len);
+                bytesToRead -= len;
             } catch (IOException ioe) {
                 exception = ioe;
-                len = -1;
+                break;
             }
         }
 
diff --git a/test/org/apache/catalina/servlets/TestDefaultServlet.java 
b/test/org/apache/catalina/servlets/TestDefaultServlet.java
index 650b4adc75..6f9701ce1b 100644
--- a/test/org/apache/catalina/servlets/TestDefaultServlet.java
+++ b/test/org/apache/catalina/servlets/TestDefaultServlet.java
@@ -16,12 +16,18 @@
  */
 package org.apache.catalina.servlets;
 
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -29,6 +35,8 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
 
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
 import jakarta.servlet.http.HttpServletResponse;
 
 import org.junit.Assert;
@@ -776,4 +784,164 @@ public class TestDefaultServlet extends TomcatBaseTest {
         client.processRequest(true);
         Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
     }
+
+    @Test
+    public void testCopyRangeContinuesAfterShortRead() throws IOException {
+        byte[] source = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
+        TesterServletOutputStream output = new TesterServletOutputStream();
+
+        IOException exception = copy(new ShortReadingInputStream(source), 
output, 0, 7);
+
+        Assert.assertNull(exception);
+        Assert.assertArrayEquals(source, output.toByteArray());
+    }
+
+
+    @Test
+    public void testCopyRangeReturnsEofExceptionAfterShortRead() throws 
IOException {
+        byte[] source = new byte[] { 0, 1, 2 };
+        TesterServletOutputStream output = new TesterServletOutputStream();
+
+        IOException exception = copy(new ShortReadingInputStream(source), 
output, 0, 7);
+
+        Assert.assertTrue(exception instanceof EOFException);
+        Assert.assertArrayEquals(source, output.toByteArray());
+    }
+
+
+    @Test
+    public void testCopyRangeReturnsEofExceptionForEmptyStream() throws 
IOException {
+        TesterServletOutputStream output = new TesterServletOutputStream();
+
+        IOException exception = copy(new ByteArrayInputStream(new byte[0]), 
output, 0, 7);
+
+        Assert.assertTrue(exception instanceof EOFException);
+        Assert.assertEquals(0, output.size());
+    }
+
+
+    @Test
+    public void testCopyRangeAtBufferBoundaries() throws IOException {
+        int bufferSize = new DefaultServlet().input;
+        int[] sourceLengths = new int[] { bufferSize - 1, bufferSize, 
bufferSize + 1 };
+
+        for (int sourceLength : sourceLengths) {
+            byte[] source = new byte[sourceLength];
+            for (int i = 0; i < source.length; i++) {
+                source[i] = (byte) i;
+            }
+            TesterServletOutputStream output = new TesterServletOutputStream();
+
+            IOException exception = copy(new ByteArrayInputStream(source), 
output, 0, sourceLength - 1);
+
+            Assert.assertNull(exception);
+            Assert.assertArrayEquals(source, output.toByteArray());
+        }
+    }
+
+
+    @Test
+    public void testCopyRangeWithNonZeroStart() throws IOException {
+        byte[] source = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
+        TesterServletOutputStream output = new TesterServletOutputStream();
+
+        IOException exception = copy(new ShortReadingInputStream(source), 
output, 2, 9);
+
+        Assert.assertNull(exception);
+        Assert.assertArrayEquals(Arrays.copyOfRange(source, 2, 10), 
output.toByteArray());
+    }
+
+
+    @Test
+    public void testCopyRangeReturnsInputIOException() throws IOException {
+        IOException expected = new IOException();
+        InputStream input = new InputStream() {
+
+            @Override
+            public int read() throws IOException {
+                throw expected;
+            }
+
+            @Override
+            public int read(byte[] target, int offset, int length) throws 
IOException {
+                throw expected;
+            }
+        };
+
+        IOException actual = copy(input, new TesterServletOutputStream(), 0, 
7);
+
+        Assert.assertSame(expected, actual);
+    }
+
+
+    private static IOException copy(InputStream input, ServletOutputStream 
output, long start, long end)
+            throws IOException {
+        DefaultServlet servlet = new DefaultServlet();
+        try (InputStream bufferedInput = new BufferedInputStream(input, 
servlet.input)) {
+            return servlet.copyNoThrow(bufferedInput, output, start, end);
+        }
+    }
+
+
+    private static class ShortReadingInputStream extends InputStream {
+
+        private static final int MAX_READ_SIZE = 3;
+
+        private final byte[] bytes;
+        private int position;
+
+        private ShortReadingInputStream(byte[] bytes) {
+            this.bytes = bytes;
+        }
+
+        @Override
+        public int read() {
+            return position < bytes.length ? bytes[position++] & 0xFF : -1;
+        }
+
+        @Override
+        public int read(byte[] target, int offset, int length) {
+            if (position == bytes.length) {
+                return -1;
+            }
+            int count = Math.min(Math.min(length, MAX_READ_SIZE), bytes.length 
- position);
+            System.arraycopy(bytes, position, target, offset, count);
+            position += count;
+            return count;
+        }
+    }
+
+
+    private static class TesterServletOutputStream extends ServletOutputStream 
{
+
+        private final ByteArrayOutputStream output = new 
ByteArrayOutputStream();
+
+        @Override
+        public void write(int value) {
+            output.write(value);
+        }
+
+        @Override
+        public void write(byte[] bytes, int offset, int length) {
+            output.write(bytes, offset, length);
+        }
+
+        @Override
+        public boolean isReady() {
+            return true;
+        }
+
+        @Override
+        public void setWriteListener(WriteListener writeListener) {
+            // NO-OP
+        }
+
+        private int size() {
+            return output.size();
+        }
+
+        private byte[] toByteArray() {
+            return output.toByteArray();
+        }
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 1571c22fa6..de3c973c2a 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -139,6 +139,12 @@
       <fix>
         Improve error handling on start-up. (markt)
       </fix>
+      <fix>
+        Fix byte range responses generated by <code>DefaultServlet</code> so a
+        short resource stream read does not truncate the response. Treat a
+        premature end of stream as an I/O error. Based on pull request
+        <pr>1067</pr> by aoto-tech. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to