This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new ce6483966d Fix truncated byte range responses in DefaultServlet
ce6483966d is described below
commit ce6483966dace9791738fc1fa18cc3c595b1d7d0
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 831c62a440..dde5496cbf 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;
@@ -2962,23 +2963,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 8a6fd9a9c5..aba6d1e41f 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 javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
@@ -775,4 +783,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 24c8a2b114..b70d4fb85a 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]