This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 7.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 64e468afc66b3d3a50d140d160bf345a4c6b53a1 Author: Mark Thomas <ma...@apache.org> AuthorDate: Wed Jul 10 16:33:03 2019 +0100 Improve async Servlet error handling If an unhandled exception occurs on a asynchronous thread started via AsyncContext.start(Runnable), process it using the standard error page mechanism. --- .../org/apache/catalina/core/AsyncContextImpl.java | 14 +- .../apache/catalina/core/LocalStrings.properties | 1 + .../http/TestHttpServletResponseSendError.java | 368 +++++++++++++++++++++ webapps/docs/changelog.xml | 5 + 4 files changed, 386 insertions(+), 2 deletions(-) diff --git a/java/org/apache/catalina/core/AsyncContextImpl.java b/java/org/apache/catalina/core/AsyncContextImpl.java index bba05e9..c39b48e 100644 --- a/java/org/apache/catalina/core/AsyncContextImpl.java +++ b/java/org/apache/catalina/core/AsyncContextImpl.java @@ -266,7 +266,7 @@ public class AsyncContextImpl implements AsyncContext, AsyncContextCallback { logDebug("start "); } check(); - Runnable wrapper = new RunnableWrapper(run, context); + Runnable wrapper = new RunnableWrapper(run, context, this.request.getCoyoteRequest()); this.request.getCoyoteRequest().action(ActionCode.ASYNC_RUN, wrapper); } @@ -556,10 +556,13 @@ public class AsyncContextImpl implements AsyncContext, AsyncContextCallback { private final Runnable wrapped; private final Context context; + private final org.apache.coyote.Request coyoteRequest; - public RunnableWrapper(Runnable wrapped, Context ctxt) { + public RunnableWrapper(Runnable wrapped, Context ctxt, + org.apache.coyote.Request coyoteRequest) { this.wrapped = wrapped; this.context = ctxt; + this.coyoteRequest = coyoteRequest; } @Override @@ -582,6 +585,13 @@ public class AsyncContextImpl implements AsyncContext, AsyncContextCallback { (context.getLoader().getClassLoader()); } wrapped.run(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + context.getLogger().error(sm.getString("asyncContextImpl.asyncRunnableError"), t); + coyoteRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + org.apache.coyote.Response coyoteResponse = coyoteRequest.getResponse(); + coyoteResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + coyoteResponse.setError(); } finally { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl( diff --git a/java/org/apache/catalina/core/LocalStrings.properties b/java/org/apache/catalina/core/LocalStrings.properties index f788a76..3d40c12 100644 --- a/java/org/apache/catalina/core/LocalStrings.properties +++ b/java/org/apache/catalina/core/LocalStrings.properties @@ -93,6 +93,7 @@ aprListener.tooLateForSSLRandomSeed=Cannot setSSLRandomSeed: SSL has already bee aprListener.wrongFIPSMode=Unexpected value of FIPSMode option of AprLifecycleListener: "{0}" asyncContextImpl.asyncDispachError=Error during asynchronous dispatch +asyncContextImpl.asyncRunnableError=Error during processing of asynchronous Runnable via AsyncContext.start() asyncContextImpl.dispatchingStarted=Asynchronous dispatch operation has already been called. Additional asynchronous dispatch operation within the same asynchronous cycle is not allowed. asyncContextImpl.noAsyncDispatcher=The dispatcher returned from the ServletContext does not support asynchronous dispatching asyncContextImpl.onCompleteError=onComplete() call failed for listener of type [{0}] diff --git a/test/javax/servlet/http/TestHttpServletResponseSendError.java b/test/javax/servlet/http/TestHttpServletResponseSendError.java new file mode 100644 index 0000000..8e0e89e --- /dev/null +++ b/test/javax/servlet/http/TestHttpServletResponseSendError.java @@ -0,0 +1,368 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package javax.servlet.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.deploy.ErrorPage; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * These tests evolved out of a discussion in the Jakarta Servlet project + * regarding the intended behaviour in various error scenarios. Async requests + * and/or async error pages added additional complexity. + */ +@RunWith(Parameterized.class) +public class TestHttpServletResponseSendError extends TomcatBaseTest { + + /* + * Implementation notes: + * Original Request + * - async + * - error in original thread / new thread + * - error before / after startAsync + * - error before / after complete / dispatch + * Error page + * - sync + * - async + * - complete + * - dispatch + */ + + private static final Boolean[] booleans = new Boolean[] { Boolean.FALSE, Boolean.TRUE }; + + private enum AsyncErrorPoint { + /* + * Thread A is the container thread the processes the original request. + * Thread B is the async thread (may or may not be a container thread) + * that is started by the async processing. + */ + THREAD_A_BEFORE_START_ASYNC, + THREAD_A_AFTER_START_ASYNC, + THREAD_A_AFTER_START_RUNNABLE, + THREAD_B_BEFORE_COMPLETE + /* + * If the error is triggered after Thread B completes async processing + * there is essentially a race condition between thread B making the + * change and the container checking to see if the error flag has been + * set. We can't easily control the execution order here so we don't + * test it. + */ + } + + + @Parameterized.Parameters(name = "{index}: async[{0}], throw[{1}], dispatch[{2}], errorPoint[{3}], useStart[{4}]") + public static Collection<Object[]> parameters() { + List<Object[]> parameterSets = new ArrayList<Object[]>(); + + for (Boolean async : booleans) { + for (Boolean throwException : booleans) { + if (async.booleanValue()) { + for (Boolean useDispatch : booleans) { + for (AsyncErrorPoint errorPoint : AsyncErrorPoint.values()) { + for (Boolean useStart : booleans) { + if (throwException.booleanValue() && !useStart.booleanValue() && + errorPoint == AsyncErrorPoint.THREAD_B_BEFORE_COMPLETE) { + // Skip this combination as exceptions that occur on application + // managed threads are not visible to the container. + continue; + } + parameterSets.add(new Object[] { async, throwException, useDispatch, + errorPoint, useStart} ); + } + } + } + } else { + // Ignore the async specific parameters + parameterSets.add(new Object[] { async, throwException, Boolean.FALSE, + AsyncErrorPoint.THREAD_A_AFTER_START_ASYNC, Boolean.FALSE} ); + } + } + } + + return parameterSets; + } + + + @Parameter(0) + public boolean async; + @Parameter(1) + public boolean throwException; + @Parameter(2) + public boolean useDispatch; + @Parameter(3) + public AsyncErrorPoint errorPoint; + @Parameter(4) + public boolean useStart; + + + @Test + public void testSendError() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + if (async) { + Wrapper w = Tomcat.addServlet(ctx, "target", + new TesterAsyncServlet(throwException, useDispatch, errorPoint, useStart)); + w.setAsyncSupported(true); + } else { + Tomcat.addServlet(ctx, "target", new TesterServlet(throwException)); + } + ctx.addServletMapping("/target", "target"); + Tomcat.addServlet(ctx, "dispatch", new TesterDispatchServlet()); + ctx.addServletMapping("/dispatch", "dispatch"); + + Tomcat.addServlet(ctx, "error599", new ErrorServletStatic599()); + ctx.addServletMapping("/error599", "error599"); + Tomcat.addServlet(ctx, "errorException", new ErrorServletStaticException()); + ctx.addServletMapping("/errorException", "errorException"); + + ErrorPage ep1 = new ErrorPage(); + ep1.setErrorCode(599); + ep1.setLocation("/error599"); + ctx.addErrorPage(ep1); + + ErrorPage ep2 = new ErrorPage(); + ep2.setExceptionType(SendErrorException.class.getName()); + ep2.setLocation("/errorException"); + ctx.addErrorPage(ep2); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + rc = getUrl("http://localhost:" + getPort() + "/target", bc, null, null); + + String body = bc.toString(); + + if (throwException) { + Assert.assertEquals(500, rc); + Assert.assertEquals("FAIL-Exception", body); + } else { + Assert.assertEquals(599, rc); + Assert.assertEquals("FAIL-599", body); + } + } + + + public static class TesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean throwException; + + public TesterServlet(boolean throwException) { + this.throwException = throwException; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (throwException) { + throw new SendErrorException(); + } else { + // Custom 5xx code so we can detect if the correct error is + // reported + resp.sendError(599); + } + } + } + + + public static class TesterAsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean throwException; + private final boolean useDispatch; + private final AsyncErrorPoint errorPoint; + private final boolean useStart; + + public TesterAsyncServlet(boolean throwException, boolean useDispatch, AsyncErrorPoint errorPoint, + boolean useStart) { + this.throwException = throwException; + this.useDispatch = useDispatch; + this.errorPoint = errorPoint; + this.useStart = useStart; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (errorPoint == AsyncErrorPoint.THREAD_A_BEFORE_START_ASYNC) { + doError(resp); + } + + AsyncContext ac = req.startAsync(); + ac.setTimeout(2000); + + if (errorPoint == AsyncErrorPoint.THREAD_A_AFTER_START_ASYNC) { + doError(resp); + } + + AsyncRunnable r = new AsyncRunnable(ac, throwException, useDispatch, errorPoint); + + if (useStart) { + ac.start(r); + } else { + Thread t = new Thread(r); + t.start(); + } + + if (errorPoint == AsyncErrorPoint.THREAD_A_AFTER_START_RUNNABLE) { + doError(resp); + } + } + + + private void doError(HttpServletResponse resp) throws IOException { + if (throwException) { + throw new SendErrorException(); + } else { + resp.sendError(599); + } + } + } + + + public static class AsyncRunnable implements Runnable { + + private final AsyncContext ac; + private final boolean throwException; + private final boolean useDispatch; + private final AsyncErrorPoint errorPoint; + + public AsyncRunnable(AsyncContext ac, boolean throwException, boolean useDispatch, + AsyncErrorPoint errorPoint) { + this.ac = ac; + this.throwException = throwException; + this.useDispatch = useDispatch; + this.errorPoint = errorPoint; + } + + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + +/* + if (errorPoint == AsyncErrorPoint.THREAD_B_AFTER_COMPLETE) { + if (useDispatch) { + ac.complete(); + } else { + ac.dispatch("/dispatch"); + } + } +*/ + if (throwException) { + throw new SendErrorException(); + } else { + // Custom 5xx code so we can detect if the correct error is + // reported + try { + ((HttpServletResponse) ac.getResponse()).sendError(599); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (errorPoint == AsyncErrorPoint.THREAD_B_BEFORE_COMPLETE) { + if (useDispatch) { + ac.dispatch("/dispatch"); + } else { + ac.complete(); + } + } + } + } + + + public static class TesterDispatchServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("DISPATCH"); + } + } + + + public static class SendErrorException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + } + + + public static class ErrorServletStatic599 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("FAIL-599"); + } + } + + + public static class ErrorServletStaticException extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("FAIL-Exception"); + } + } + +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 79f6589..5fed093 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -147,6 +147,11 @@ <update> Update the recommended minimum Tomcat Native version to 1.2.23. (markt) </update> + <fix> + If an unhandled exception occurs on a asynchronous thread started via + <code>AsyncContext.start(Runnable)</code>, process it using the standard + error page mechanism. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org