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

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

commit 3082475acb55e9b0aaa17600b498c30189e20eca
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Wed Feb 24 18:43:22 2021 +0000

    Improve error handling if non-blocking IO code swallows IOExceptions
    
    Tomcat previously expected IOExceptions in onWritePossible() and
    onDataAvailable() to not be swallowed. These additions allow correct
    error handling even if the application code swallows the exception.
---
 .../apache/catalina/connector/CoyoteAdapter.java   |  8 +++
 .../org/apache/catalina/connector/InputBuffer.java | 57 +++++++----------
 .../apache/catalina/connector/OutputBuffer.java    |  1 +
 java/org/apache/coyote/Request.java                | 33 ++++++++++
 java/org/apache/coyote/Response.java               | 11 ++--
 .../catalina/nonblocking/TestNonBlockingAPI.java   | 73 ++++++++++++++--------
 webapps/docs/changelog.xml                         |  6 ++
 7 files changed, 124 insertions(+), 65 deletions(-)

diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java 
b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 19bb1d3..d61a5e9 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -183,6 +183,10 @@ public class CoyoteAdapter implements Adapter {
                                 readListener != null) {
                             readListener.onAllDataRead();
                         }
+                        // User code may have swallowed an IOException
+                        if (response.getCoyoteResponse().isExceptionPresent()) 
{
+                            throw 
response.getCoyoteResponse().getErrorException();
+                        }
                     } catch (Throwable t) {
                         ExceptionUtils.handleThrowable(t);
                         // Need to trigger the call to 
AbstractProcessor.setErrorState()
@@ -211,6 +215,10 @@ public class CoyoteAdapter implements Adapter {
                         if (request.isFinished() && 
req.sendAllDataReadEvent()) {
                             readListener.onAllDataRead();
                         }
+                        // User code may have swallowed an IOException
+                        if (request.getCoyoteRequest().isExceptionPresent()) {
+                            throw 
request.getCoyoteRequest().getErrorException();
+                        }
                     } catch (Throwable t) {
                         ExceptionUtils.handleThrowable(t);
                         // Need to trigger the call to 
AbstractProcessor.setErrorState()
diff --git a/java/org/apache/catalina/connector/InputBuffer.java 
b/java/org/apache/catalina/connector/InputBuffer.java
index 4da5b22..6c9c6d1 100644
--- a/java/org/apache/catalina/connector/InputBuffer.java
+++ b/java/org/apache/catalina/connector/InputBuffer.java
@@ -335,6 +335,7 @@ public class InputBuffer extends Reader
         try {
             return coyoteRequest.doRead(this);
         } catch (IOException ioe) {
+            coyoteRequest.setErrorException(ioe);
             // An IOException on a read is almost always due to
             // the remote client aborting the request.
             throw new ClientAbortException(ioe);
@@ -343,9 +344,7 @@ public class InputBuffer extends Reader
 
 
     public int readByte() throws IOException {
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (checkByteBufferEof()) {
             return -1;
@@ -355,9 +354,7 @@ public class InputBuffer extends Reader
 
 
     public int read(byte[] b, int off, int len) throws IOException {
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (checkByteBufferEof()) {
             return -1;
@@ -380,9 +377,7 @@ public class InputBuffer extends Reader
      * @throws IOException if an input or output exception has occurred
      */
     public int read(ByteBuffer to) throws IOException {
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (checkByteBufferEof()) {
             return -1;
@@ -436,10 +431,7 @@ public class InputBuffer extends Reader
 
     @Override
     public int read() throws IOException {
-
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (checkCharBufferEof()) {
             return -1;
@@ -450,21 +442,14 @@ public class InputBuffer extends Reader
 
     @Override
     public int read(char[] cbuf) throws IOException {
-
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
-
+        throwIfClosed();
         return read(cbuf, 0, cbuf.length);
     }
 
 
     @Override
     public int read(char[] cbuf, int off, int len) throws IOException {
-
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (checkCharBufferEof()) {
             return -1;
@@ -477,9 +462,7 @@ public class InputBuffer extends Reader
 
     @Override
     public long skip(long n) throws IOException {
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (n < 0) {
             throw new IllegalArgumentException();
@@ -505,9 +488,7 @@ public class InputBuffer extends Reader
 
     @Override
     public boolean ready() throws IOException {
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
         if (state == INITIAL_STATE) {
             state = CHAR_STATE;
         }
@@ -524,9 +505,7 @@ public class InputBuffer extends Reader
     @Override
     public void mark(int readAheadLimit) throws IOException {
 
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (cb.remaining() <= 0) {
             clear(cb);
@@ -544,15 +523,15 @@ public class InputBuffer extends Reader
     @Override
     public void reset() throws IOException {
 
-        if (closed) {
-            throw new IOException(sm.getString("inputBuffer.streamClosed"));
-        }
+        throwIfClosed();
 
         if (state == CHAR_STATE) {
             if (markPos < 0) {
                 clear(cb);
                 markPos = -1;
-                throw new IOException();
+                IOException ioe = new IOException();
+                coyoteRequest.setErrorException(ioe);
+                throw ioe;
             } else {
                 cb.position(markPos);
             }
@@ -562,6 +541,14 @@ public class InputBuffer extends Reader
     }
 
 
+    private void throwIfClosed() throws IOException {
+        if (closed) {
+            IOException ioe = new 
IOException(sm.getString("inputBuffer.streamClosed"));
+            coyoteRequest.setErrorException(ioe);
+            throw ioe;
+        }
+    }
+
     public void checkConverter() throws IOException {
         if (conv != null) {
             return;
diff --git a/java/org/apache/catalina/connector/OutputBuffer.java 
b/java/org/apache/catalina/connector/OutputBuffer.java
index 5fa8fc2..2d5a6fe 100644
--- a/java/org/apache/catalina/connector/OutputBuffer.java
+++ b/java/org/apache/catalina/connector/OutputBuffer.java
@@ -348,6 +348,7 @@ public class OutputBuffer extends Writer {
                 // An IOException on a write is almost always due to
                 // the remote client aborting the request. Wrap this
                 // so that it can be handled better by the error dispatcher.
+                coyoteResponse.setErrorException(e);
                 throw new ClientAbortException(e);
             }
         }
diff --git a/java/org/apache/coyote/Request.java 
b/java/org/apache/coyote/Request.java
index eea1487..72ca5c9 100644
--- a/java/org/apache/coyote/Request.java
+++ b/java/org/apache/coyote/Request.java
@@ -161,6 +161,11 @@ public final class Request {
 
     private boolean sendfile = true;
 
+    /**
+     * Holds request body reading error exception.
+     */
+    private Exception errorException = null;
+
     volatile ReadListener listener;
 
     public ReadListener getReadListener() {
@@ -565,6 +570,34 @@ public final class Request {
     }
 
 
+    // -------------------- Error tracking --------------------
+
+    /**
+     * Set the error Exception that occurred during the writing of the response
+     * processing.
+     *
+     * @param ex The exception that occurred
+     */
+    public void setErrorException(Exception ex) {
+        errorException = ex;
+    }
+
+
+    /**
+     * Get the Exception that occurred during the writing of the response.
+     *
+     * @return The exception that occurred
+     */
+    public Exception getErrorException() {
+        return errorException;
+    }
+
+
+    public boolean isExceptionPresent() {
+        return errorException != null;
+    }
+
+
     // -------------------- debug --------------------
 
     @Override
diff --git a/java/org/apache/coyote/Response.java 
b/java/org/apache/coyote/Response.java
index b9f22b7..c7943b4 100644
--- a/java/org/apache/coyote/Response.java
+++ b/java/org/apache/coyote/Response.java
@@ -125,9 +125,9 @@ public final class Response {
     private long commitTime = -1;
 
     /**
-     * Holds request error exception.
+     * Holds response writing error exception.
      */
-    Exception errorException = null;
+    private Exception errorException = null;
 
     /**
      * With the introduction of async processing and the possibility of
@@ -275,7 +275,8 @@ public final class Response {
     // -----------------Error State --------------------
 
     /**
-     * Set the error Exception that occurred during request processing.
+     * Set the error Exception that occurred during the writing of the response
+     * processing.
      *
      * @param ex The exception that occurred
      */
@@ -285,7 +286,7 @@ public final class Response {
 
 
     /**
-     * Get the Exception that occurred during request processing.
+     * Get the Exception that occurred during the writing of the response.
      *
      * @return The exception that occurred
      */
@@ -295,7 +296,7 @@ public final class Response {
 
 
     public boolean isExceptionPresent() {
-        return ( errorException != null );
+        return errorException != null;
     }
 
 
diff --git a/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java 
b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
index c33539b..f6a8fec 100644
--- a/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
+++ b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
@@ -1313,6 +1313,18 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
     }
 
 
+    @Test
+    public void testNonBlockingWriteError02NoSwallow() throws Exception {
+        doTestNonBlockingWriteError02(false);
+    }
+
+
+    @Test
+    public void testNonBlockingWriteError02Swallow() throws Exception {
+        doTestNonBlockingWriteError02(true);
+    }
+
+
     /*
      * Tests client disconnect in the following scenario:
      * - async with non-blocking IO
@@ -1321,8 +1333,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
      * - client disconnects
      * - server attempts a write
      */
-    @Test
-    public void testNonBlockingWriteError02() throws Exception {
+    private void doTestNonBlockingWriteError02(boolean swallowIoException) 
throws Exception {
         CountDownLatch responseCommitLatch = new CountDownLatch(1);
         CountDownLatch clientCloseLatch = new CountDownLatch(1);
         CountDownLatch asyncCompleteLatch = new CountDownLatch(1);
@@ -1333,7 +1344,8 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
         // No file system docBase required
         Context ctx = tomcat.addContext("", null);
 
-        NBWriteServlet02 writeServlet = new 
NBWriteServlet02(responseCommitLatch, clientCloseLatch, asyncCompleteLatch);
+        NBWriteServlet02 writeServlet =
+                new NBWriteServlet02(responseCommitLatch, clientCloseLatch, 
asyncCompleteLatch, swallowIoException);
         Wrapper wrapper = Tomcat.addServlet(ctx, "writeServlet", writeServlet);
         wrapper.setAsyncSupported(true);
         ctx.addServletMappingDecoded("/*", "writeServlet");
@@ -1355,7 +1367,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
         client.disconnect();
         clientCloseLatch.countDown();
 
-        Assert.assertTrue("Failed to complete async processing", 
asyncCompleteLatch.await(10, TimeUnit.SECONDS));
+        Assert.assertTrue("Failed to complete async processing", 
asyncCompleteLatch.await(60, TimeUnit.SECONDS));
     }
 
 
@@ -1366,12 +1378,14 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
         private final CountDownLatch responseCommitLatch;
         private final CountDownLatch clientCloseLatch;
         private final CountDownLatch asyncCompleteLatch;
+        private final boolean swallowIoException;
 
         public NBWriteServlet02(CountDownLatch responseCommitLatch, 
CountDownLatch clientCloseLatch,
-                CountDownLatch asyncCompleteLatch) {
+                CountDownLatch asyncCompleteLatch, boolean swallowIoException) 
{
             this.responseCommitLatch = responseCommitLatch;
             this.clientCloseLatch = clientCloseLatch;
             this.asyncCompleteLatch = asyncCompleteLatch;
+            this.swallowIoException = swallowIoException;
         }
 
         @Override
@@ -1383,7 +1397,8 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
             ac.addListener(new TestAsyncListener02(asyncCompleteLatch));
             ac.setTimeout(5000);
 
-            WriteListener writeListener = new TestWriteListener02(ac, 
responseCommitLatch, clientCloseLatch);
+            WriteListener writeListener =
+                    new TestWriteListener02(ac, responseCommitLatch, 
clientCloseLatch, swallowIoException);
             resp.getOutputStream().setWriteListener(writeListener);
         }
     }
@@ -1424,37 +1439,45 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
         private final AsyncContext ac;
         private final CountDownLatch responseCommitLatch;
         private final CountDownLatch clientCloseLatch;
+        private final boolean swallowIoException;
         private volatile int stage = 0;
 
         public TestWriteListener02(AsyncContext ac, CountDownLatch 
responseCommitLatch,
-                CountDownLatch clientCloseLatch) {
+                CountDownLatch clientCloseLatch, boolean swallowIoException) {
             this.ac = ac;
             this.responseCommitLatch = responseCommitLatch;
             this.clientCloseLatch = clientCloseLatch;
+            this.swallowIoException = swallowIoException;
         }
 
         @Override
         public void onWritePossible() throws IOException {
-            ServletOutputStream sos = ac.getResponse().getOutputStream();
-            do {
-                if (stage == 0) {
-                    // Commit the response
-                    ac.getResponse().flushBuffer();
-                    responseCommitLatch.countDown();
-                    stage++;
-                } else if (stage == 1) {
-                    // Wait for the client to drop the connection
-                    try {
-                        clientCloseLatch.await();
-                    } catch (InterruptedException e) {
-                        // Ignore
+            try {
+                ServletOutputStream sos = ac.getResponse().getOutputStream();
+                do {
+                    if (stage == 0) {
+                        // Commit the response
+                        ac.getResponse().flushBuffer();
+                        responseCommitLatch.countDown();
+                        stage++;
+                    } else if (stage == 1) {
+                        // Wait for the client to drop the connection
+                        try {
+                            clientCloseLatch.await();
+                        } catch (InterruptedException e) {
+                            // Ignore
+                        }
+                        sos.print("TEST");
+                        stage++;
+                    } else if (stage == 2) {
+                        sos.flush();
                     }
-                    sos.print("TEST");
-                    stage++;
-                } else if (stage == 2) {
-                    sos.flush();
+                } while (sos.isReady());
+            } catch (IOException ioe) {
+                if (!swallowIoException) {
+                    throw ioe;
                 }
-            } while (sos.isReady());
+            }
         }
 
         @Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index dba2eb7..43e5ed0 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -125,6 +125,12 @@
         when a I/O error occurs during non-blocking I/O. There were some cases
         discovered where this was not happening. (markt)
       </fix>
+      <add>
+        Make the non-blocking I/O error handling more robust by handling the
+        case where the application code swallows an <code>IOException</code> in
+        <code>WriteListener.onWritePossible()</code> and
+        <code>ReadListener.onDataAvailable()</code>. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">


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

Reply via email to