23 Aug 2024 14:34:07 Rémy Maucherat <r...@apache.org>:

On Thu, Aug 22, 2024 at 2:33 PM <ma...@apache.org> wrote:

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 69eff83577 Expected behaviour has been clarified when writing >= c-l bytes to body
69eff83577 is described below

commit 69eff83577f7c00cbaaca9384ab4b1989f516797
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Thu Aug 22 13:33:10 2024 +0100

    Expected behaviour has been clarified when writing >= c-l bytes to body

I'm not sure yet whether this is a good idea. Usually we want to keep
things open for further processing.

Maybe we should be doing it using the suspended flag instead and in the facades.

Ack. I had a similar concern. That is the main reason I didn't back-port it. However, the TCK for 6.1 has been updated to explicitly check for an exception in this case. The update 6.1 TCK hasn't been released yet but when it has we'll need this change to pass.

We could try pushing back on the TCK change but it is testing language that has been in the specification for a long time.

FYI the specification for 6.2 is going to clarify a bunch of edge cases around this such as expected behaviour when writing > content-length bytes in a single write.

Mark



Rémy

---
.../catalina/connector/LocalStrings.properties     |  1 +
.../apache/catalina/connector/OutputBuffer.java    | 30 ++++++++++---
.../catalina/connector/TestCoyoteOutputStream.java | 50 ++++++++++++++++++++++
3 files changed, 76 insertions(+), 5 deletions(-)

diff --git a/java/org/apache/catalina/connector/LocalStrings.properties b/java/org/apache/catalina/connector/LocalStrings.properties
index 077cf46136..d20cd26bc4 100644
--- a/java/org/apache/catalina/connector/LocalStrings.properties
+++ b/java/org/apache/catalina/connector/LocalStrings.properties
@@ -83,6 +83,7 @@ coyoteResponse.setBufferSize.ise=Cannot change buffer size after data has been w
inputBuffer.requiresNonBlocking=Not available in non blocking mode
inputBuffer.streamClosed=Stream closed

+outputBuffer.closed=The response may not be written to once it has been closed outputBuffer.writeNull=The String argument to write(String,int,int) may not be null

request.asyncNotSupported=A filter or servlet of the current chain does not support asynchronous operations. diff --git a/java/org/apache/catalina/connector/OutputBuffer.java b/java/org/apache/catalina/connector/OutputBuffer.java
index b185561ef3..04cf82e632 100644
--- a/java/org/apache/catalina/connector/OutputBuffer.java
+++ b/java/org/apache/catalina/connector/OutputBuffer.java
@@ -358,11 +358,11 @@ public class OutputBuffer extends Writer {
     private void writeBytes(byte b[], int off, int len) throws IOException {

         if (closed) {
-            return;
+            throw new IOException(sm.getString("outputBuffer.closed"));
         }

         append(b, off, len);
-        bytesWritten += len;
+        updateBytesWritten(len);

         // if called from within flush(), then immediately flush
         // remaining bytes
@@ -376,12 +376,12 @@ public class OutputBuffer extends Writer {
     private void writeBytes(ByteBuffer from) throws IOException {

         if (closed) {
-            return;
+            throw new IOException(sm.getString("outputBuffer.closed"));
         }

         int remaining = from.remaining();
         append(from);
-        bytesWritten += remaining;
+        updateBytesWritten(remaining);

         // if called from within flush(), then immediately flush
         // remaining bytes
@@ -394,6 +394,10 @@ public class OutputBuffer extends Writer {

     public void writeByte(int b) throws IOException {

+        if (closed) {
+            throw new IOException(sm.getString("outputBuffer.closed"));
+        }
+
         if (suspended) {
             return;
         }
@@ -403,8 +407,24 @@ public class OutputBuffer extends Writer {
         }

         transfer((byte) b, bb);
-        bytesWritten++;
+        updateBytesWritten(1);
+    }
+

+    private void updateBytesWritten(int increment) throws IOException {
+        bytesWritten += increment;
+        int contentLength = coyoteResponse.getContentLength();
+
+        /*
+         * Handle the requirements of section 5.7 of the Servlet specification - Closure of the Response Object.
+         *
+         * Currently this just handles the simple case. There is work in progress to better define what should happen if +         * an attempt is made to write > content-length bytes. When that work is complete, this is likely where the
+         * implementation will end up.
+         */
+        if (contentLength != -1 && bytesWritten >= contentLength) {
+            close();
+        }
     }


diff --git a/test/org/apache/catalina/connector/TestCoyoteOutputStream.java b/test/org/apache/catalina/connector/TestCoyoteOutputStream.java
index d808db76f7..bf282f5d67 100644
--- a/test/org/apache/catalina/connector/TestCoyoteOutputStream.java
+++ b/test/org/apache/catalina/connector/TestCoyoteOutputStream.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.servlet.AsyncContext;
@@ -290,4 +291,53 @@ public class TestCoyoteOutputStream extends TomcatBaseTest {
         }

     }
+
+
+    @Test
+    public void testWriteAfterBodyComplete() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        Context root = tomcat.addContext("", TEMP_DIR);
+        Tomcat.addServlet(root, "servlet", new WriteAfterBodyCompleteServlet());
+        root.addServletMappingDecoded("/", "servlet");
+
+        tomcat.start();
+
+        ByteChunk bc = new ByteChunk();
+        int rc = getUrl("http://localhost:"; + getPort() + "/", bc, null, null);
+        Assert.assertEquals(HttpServletResponse.SC_OK, rc);
+
+        // Wait upto 5s
+        int count = 0;
+        boolean exceptionSeen = WriteAfterBodyCompleteServlet.exceptionSeen.get();
+        while (count < 50 && !exceptionSeen) {
+            Thread.sleep(100);
+            exceptionSeen = WriteAfterBodyCompleteServlet.exceptionSeen.get();
+            count++;
+        }
+        Assert.assertTrue(exceptionSeen);
+    }
+
+
+    private static final class WriteAfterBodyCompleteServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+        private static final AtomicBoolean exceptionSeen = new AtomicBoolean(false);
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            resp.setContentType("text/plain");
+            resp.setContentLengthLong(10);
+            ServletOutputStream sos = resp.getOutputStream();
+            sos.write("0123456789".getBytes(StandardCharsets.UTF_8));
+
+            // sos should now be closed. A further write should trigger an error.
+            try {
+                sos.write("anything".getBytes(StandardCharsets.UTF_8));
+            } catch (IOException ioe) {
+                exceptionSeen.set(true);
+            }
+        }
+    }
}


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to