From b4aac8ad8af7faf62b177ce2ce479ab2e7415c64 Mon Sep 17 00:00:00 2001 From: James Baker Date: Tue, 6 Sep 2022 13:18:50 +0100 Subject: [PATCH 1/3] [UNDERTOW-2157] UndertowOutputStream: behaviour discrepancies, clarify contract, fix bug BufferedWriteableOutputStream is currently implemented by: 1. UndertowOutputStream 2. ServletOutputStreamImpl These have two different behaviours. In UndertowOutputStream, I _believe_ that if the input FileChannel's position is 0, it transfers the whole channel, and if it's non-zero, it crashes. In ServletOutputStreamImpl, I think the behaviour is something more like: 1. If there is no listener set, transfer the bytes between the current position and end of file. 2. If there is a listener set, transfer the bytes between the current position and end of file, setting the input channel's position as well. This PR makes the behaviour be more consistent and adds a new, clearer API, deprecating the old one. 1. Clarify the contract of BufferedWriteableOutputStream.transferTo as being the ServletOutputStreamImpl's behaviour, since it has a superset of the UndertowOutputStream's functionality. 2. Never set the position of the input channel in ServletOutputStreamImpl, since it leads to inconsistent behaviour depending on whether the listener is set and seems incongruent with the contract of the underlying FileChannel apis. 3. Add an explicit `transferFrom(FileChannel source, long startPosition, long count)` method to the interface and implement in both implementations. In theory there is a breaking behaviour change for the user who is relying on their FileChannel's position being set to EOF as a side-effect of the change. Internally to Undertow there is no problem here. --- .../io/BufferWritableOutputStream.java | 13 ++++++++++ .../io/undertow/io/UndertowOutputStream.java | 11 +++++---- .../servlet/spec/ServletOutputStreamImpl.java | 24 +++++++++---------- .../undertow/servlet/test/util/TXServlet.java | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java b/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java index 63ca9a9d88..c4a1e85064 100644 --- a/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java +++ b/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java @@ -34,6 +34,19 @@ public interface BufferWritableOutputStream { void write(ByteBuffer byteBuffer) throws IOException; + /** + * Transfer the remaining content of the input FileChannel (from its current position, to end of file). + * @deprecated use {@link BufferWritableOutputStream#transferFrom(FileChannel, long, long)}. + */ + @Deprecated void transferFrom(FileChannel source) throws IOException; + /** + * Transfer from the input channel to the channel underlying this OutputStream. + * @param source the source file channel + * @param startPosition the start position in the source file + * @param count the number of bytes to transfer + */ + void transferFrom(FileChannel source, long startPosition, long count) throws IOException; + } diff --git a/core/src/main/java/io/undertow/io/UndertowOutputStream.java b/core/src/main/java/io/undertow/io/UndertowOutputStream.java index 22b7b49709..d61e311d5d 100644 --- a/core/src/main/java/io/undertow/io/UndertowOutputStream.java +++ b/core/src/main/java/io/undertow/io/UndertowOutputStream.java @@ -309,6 +309,11 @@ private void writeBufferBlocking(final boolean writeFinal) throws IOException { @Override public void transferFrom(FileChannel source) throws IOException { + transferFrom(source, source.position(), source.size() - source.position()); + } + + @Override + public void transferFrom(FileChannel source, long startPosition, long count) throws IOException { if (anyAreSet(state, FLAG_CLOSED)) { throw UndertowMessages.MESSAGES.streamIsClosed(); } @@ -318,10 +323,8 @@ public void transferFrom(FileChannel source) throws IOException { if (channel == null) { channel = exchange.getResponseChannel(); } - long position = source.position(); - long size = source.size(); - Channels.transferBlocking(channel, source, position, size); - updateWritten(size - position); + Channels.transferBlocking(channel, source, startPosition, count); + updateWritten(count); } /** diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java index 93171c3426..33d8945b2d 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java @@ -553,6 +553,11 @@ public void flushInternal() throws IOException { @Override public void transferFrom(FileChannel source) throws IOException { + transferFrom(source, source.position(), source.size() - source.position()); + } + + @Override + public void transferFrom(FileChannel source, long startPosition, long count) throws IOException { if (anyAreSet(state, FLAG_CLOSED) || servletRequestContext.getOriginalResponse().isTreatAsCommitted()) { throw UndertowServletMessages.MESSAGES.streamIsClosed(); } @@ -564,12 +569,7 @@ public void transferFrom(FileChannel source) throws IOException { if (channel == null) { channel = servletRequestContext.getExchange().getResponseChannel(); } - long position = source.position(); - long count = source.size() - position; - if (count > remainingContentLength) { - count = remainingContentLength; - } - Channels.transferBlocking(channel, source, position, count); + Channels.transferBlocking(channel, source, startPosition, count); updateWritten(count); } else { setFlags(FLAG_WRITE_STARTED); @@ -577,25 +577,23 @@ public void transferFrom(FileChannel source) throws IOException { long pos = 0; try { - long size = Math.min (source.size(), remainingContentLength); - pos = source.position(); + long end = pos + count; + pos = startPosition; - while (size - pos > 0) { - long ret = channel.transferFrom(pendingFile, pos, size - pos); + while (end - pos > 0) { + long ret = channel.transferFrom(pendingFile, pos, end - pos); if (ret <= 0) { clearFlags(FLAG_READY); pendingFile = source; - source.position(pos); channel.resumeWrites(); return; } pos += ret; } } finally { - updateWrittenAsync(pos - source.position()); + updateWrittenAsync(pos - startPosition); } } - } diff --git a/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java b/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java index 6118be085c..43a3650715 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java +++ b/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java @@ -50,7 +50,7 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res } BufferWritableOutputStream stream = (BufferWritableOutputStream) resp.getOutputStream(); - stream.transferFrom(file); + stream.transferFrom(file, 0, file.size()); file.close(); } From 972c57b5f9feed70d13d2f226681197105a5f83a Mon Sep 17 00:00:00 2001 From: Flavia Rainone Date: Thu, 16 Oct 2025 10:33:32 -0300 Subject: [PATCH 2/3] [UNDERTOW-2157] Revert the changes done in the transfer method signature Signed-off-by: Flavia Rainone --- .../java/io/undertow/io/BufferWritableOutputStream.java | 9 --------- .../main/java/io/undertow/io/UndertowOutputStream.java | 7 ++----- .../undertow/servlet/spec/ServletOutputStreamImpl.java | 7 ++----- .../java/io/undertow/servlet/test/util/TXServlet.java | 2 +- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java b/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java index c4a1e85064..b22be4086a 100644 --- a/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java +++ b/core/src/main/java/io/undertow/io/BufferWritableOutputStream.java @@ -36,17 +36,8 @@ public interface BufferWritableOutputStream { /** * Transfer the remaining content of the input FileChannel (from its current position, to end of file). - * @deprecated use {@link BufferWritableOutputStream#transferFrom(FileChannel, long, long)}. */ @Deprecated void transferFrom(FileChannel source) throws IOException; - /** - * Transfer from the input channel to the channel underlying this OutputStream. - * @param source the source file channel - * @param startPosition the start position in the source file - * @param count the number of bytes to transfer - */ - void transferFrom(FileChannel source, long startPosition, long count) throws IOException; - } diff --git a/core/src/main/java/io/undertow/io/UndertowOutputStream.java b/core/src/main/java/io/undertow/io/UndertowOutputStream.java index d61e311d5d..68f974875c 100644 --- a/core/src/main/java/io/undertow/io/UndertowOutputStream.java +++ b/core/src/main/java/io/undertow/io/UndertowOutputStream.java @@ -309,11 +309,8 @@ private void writeBufferBlocking(final boolean writeFinal) throws IOException { @Override public void transferFrom(FileChannel source) throws IOException { - transferFrom(source, source.position(), source.size() - source.position()); - } - - @Override - public void transferFrom(FileChannel source, long startPosition, long count) throws IOException { + final long startPosition = source.position(); + final long count = source.size() - source.position(); if (anyAreSet(state, FLAG_CLOSED)) { throw UndertowMessages.MESSAGES.streamIsClosed(); } diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java index 33d8945b2d..2be00972ff 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java @@ -553,14 +553,11 @@ public void flushInternal() throws IOException { @Override public void transferFrom(FileChannel source) throws IOException { - transferFrom(source, source.position(), source.size() - source.position()); - } - - @Override - public void transferFrom(FileChannel source, long startPosition, long count) throws IOException { if (anyAreSet(state, FLAG_CLOSED) || servletRequestContext.getOriginalResponse().isTreatAsCommitted()) { throw UndertowServletMessages.MESSAGES.streamIsClosed(); } + final long startPosition = source.position(); + final long count = source.size() - source.position(); final long remainingContentLength = remainingContentLength(); if (listener == null) { if (buffer != null && buffer.position() != 0) { diff --git a/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java b/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java index 43a3650715..6118be085c 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java +++ b/servlet/src/test/java/io/undertow/servlet/test/util/TXServlet.java @@ -50,7 +50,7 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res } BufferWritableOutputStream stream = (BufferWritableOutputStream) resp.getOutputStream(); - stream.transferFrom(file, 0, file.size()); + stream.transferFrom(file); file.close(); } From 3494607c5aecf6fc6d0d7ce84788999f192ec9b5 Mon Sep 17 00:00:00 2001 From: Flavia Rainone Date: Thu, 16 Oct 2025 10:38:33 -0300 Subject: [PATCH 3/3] [UNDERTOW-2157] Reinstate the fix for UNDERTOW-2277 that had been removed Signed-off-by: Flavia Rainone --- .../io/undertow/servlet/spec/ServletOutputStreamImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java index 2be00972ff..30b79e4a25 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletOutputStreamImpl.java @@ -557,8 +557,11 @@ public void transferFrom(FileChannel source) throws IOException { throw UndertowServletMessages.MESSAGES.streamIsClosed(); } final long startPosition = source.position(); - final long count = source.size() - source.position(); + long count = source.size() - source.position(); final long remainingContentLength = remainingContentLength(); + if (count > remainingContentLength) { + count = remainingContentLength; + } if (listener == null) { if (buffer != null && buffer.position() != 0) { writeBufferBlocking(false);