Skip to content

Fix truncated byte range responses in DefaultServlet - #1067

Open
aoto-tech wants to merge 1 commit into
apache:mainfrom
aoto-tech:fix-default-servlet-range-short-read
Open

Fix truncated byte range responses in DefaultServlet#1067
aoto-tech wants to merge 1 commit into
apache:mainfrom
aoto-tech:fix-default-servlet-range-short-read

Conversation

@aoto-tech

@aoto-tech aoto-tech commented Sep 9, 2026

Copy link
Copy Markdown

Summary

Fix DefaultServlet byte range responses so a legal short read from the
resource stream no longer truncates the response. Also report a premature end
of stream as an I/O error instead of passing -1 as the output length and
triggering an unchecked exception.

Impact

Range requests are commonly used for resumed downloads and media seeking. In
these flows, a valid range can produce an incomplete 206 response if the
resource stream performs a legal short read. DefaultServlet writes the first
short chunk and reports no I/O error, leaving the client with fewer bytes than
the selected range.

Problem

DefaultServlet.copyNoThrow(InputStream, ServletOutputStream, long, long) uses
the length returned by the previous read as part of its loop condition:

while ((bytesToRead > 0) && (len >= buffer.length))

InputStream.read(byte[]) may legally return a positive value smaller than the
requested length without reaching EOF. The current code writes the short chunk
and exits the loop while bytesToRead is still positive. The method returns
null, leaving the caller with no I/O error for the incomplete 206 response
body.

If the stream reaches EOF before the requested range is complete, read()
returns -1. The current comparison enters the write branch and calls
ServletOutputStream.write(..., -1), resulting in an
IndexOutOfBoundsException.

Both the single-range and multipart-range paths use this method. Production
callers wrap the resource stream in BufferedInputStream, but the wrapper gives
no guarantee of filling the requested buffer on every read.

Reproduction

A focused reproducer models this flow with an eight-byte resource, a
Range: bytes=0-7 request, and a buffered resource stream returning at most
three bytes per read:

selected range: 8 bytes
response body:  3 bytes
copy result:    no I/O error

For a source ending before the requested range is complete, the current
implementation throws:

java.lang.IndexOutOfBoundsException: Range [0, 0 + -1) out of bounds for length 2048

The premature EOF case models a resource stream ending after the range length
has been established.

Change

The range copy loop is now driven only by the number of bytes remaining. Each
read is limited to the smaller of the buffer size and the remaining range
length. A positive short read is written and copying continues. Premature EOF
returns an EOFException through the existing IOException result path.

No public API is changed. Streams already filling the requested buffer keep the
same behavior.

Tests

ant -noinput -Dtest.entry=org.apache.catalina.servlets.TestDefaultServletRangeCopy test
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0

ant -noinput -Dtest.entry=org.apache.catalina.servlets.TestDefaultServletRangeRequests test
Tests run: 36, Failures: 0, Errors: 0, Skipped: 0

ant -noinput -Dexecute.validate=true validate
BUILD SUCCESSFUL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant