2012/11/12 <ma...@apache.org>: > Author: markt > Date: Sun Nov 11 23:35:41 2012 > New Revision: 1408152 > > URL: http://svn.apache.org/viewvc?rev=1408152&view=rev > Log: > Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=54123 > There are two things going on here: > 1. The reported bug. If there is a async timeout and no async listeners, > trigger a 500 response. > 2. Implement "error dispatch". This is used a couple of times in the spec > without any definition. The implication from the part of the spec quoted in > the bug report is: > - The standard error page mechanism should be used to identify the page > - An async request that has been started should be left in async mode when > forwarding to the error page > - The error page may call complete() or dispatch() > This commit hooks into the StandardHostValve to access the error page > mechanism. I could have copied and pasted but I preferred the dependency on > StandardHostValve > Because the error page may do a dispatch(), need to ensure that when running > the dispatch(), the error page mechanism is not triggered a second time. > Depending on what emerges running the full unit tests and the TCK, I mat > still decide to copy the error page code to AsyncContextImpl > > Modified: > tomcat/tc7.0.x/trunk/ (props changed) > tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/AsyncContextImpl.java > tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/StandardHostValve.java > tomcat/tc7.0.x/trunk/java/org/apache/coyote/AsyncStateMachine.java > > tomcat/tc7.0.x/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java > tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml > > Propchange: tomcat/tc7.0.x/trunk/ > ------------------------------------------------------------------------------ > Merged /tomcat/trunk:r1408148 > > Modified: > tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/AsyncContextImpl.java > URL: > http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/AsyncContextImpl.java?rev=1408152&r1=1408151&r2=1408152&view=diff > ============================================================================== > --- tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/AsyncContextImpl.java > (original) > +++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/AsyncContextImpl.java > Sun Nov 11 23:35:41 2012 > @@ -41,6 +41,8 @@ import javax.servlet.http.HttpServletRes > import org.apache.catalina.AsyncDispatcher; > import org.apache.catalina.Context; > import org.apache.catalina.Globals; > +import org.apache.catalina.Host; > +import org.apache.catalina.Valve; > import org.apache.catalina.connector.Request; > import org.apache.coyote.ActionCode; > import org.apache.coyote.AsyncContextCallback; > @@ -128,8 +130,8 @@ public class AsyncContextImpl implements > ActionCode.ASYNC_IS_TIMINGOUT, result); > return !result.get(); > } else { > - // No listeners, container calls complete > - complete(); > + // No listeners, trigger error handling > + return false; > } > > } finally { > @@ -372,6 +374,23 @@ public class AsyncContextImpl implements > listener.getClass().getName() + "]", ioe); > } > }
The spec in 2.3.3.3 says "If none of the listeners called AsyncContext.complete or any of the AsyncContext.dispatch methods, then perform an error dispatch...." As far as I see, the code below is executed unconditionally, but the spec says that it has to be executed only if the listeners did not do the calls.... > + > + // SRV.2.3.3.3 (search for "error dispatch") > + if (servletResponse instanceof HttpServletResponse) { > + ((HttpServletResponse) servletResponse).setStatus( > + HttpServletResponse.SC_INTERNAL_SERVER_ERROR); > + } > + > + Host host = (Host) context.getParent(); > + Valve stdHostValve = host.getPipeline().getBasic(); > + if (stdHostValve instanceof StandardHostValve) { > + ((StandardHostValve) stdHostValve).throwable(request, > + request.getResponse(), t); > + } > + > + if (isStarted() && !request.isAsyncDispatching()) { > + complete(); > + } > } > > > > Modified: > tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/StandardHostValve.java > URL: > http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/StandardHostValve.java?rev=1408152&r1=1408151&r2=1408152&view=diff > ============================================================================== > --- tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/StandardHostValve.java > (original) > +++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/core/StandardHostValve.java > Sun Nov 11 23:35:41 2012 > @@ -161,6 +161,9 @@ final class StandardHostValve extends Va > // If a request init listener throws an exception, the request is > // aborted > boolean asyncAtStart = request.isAsync(); > + // An async error page may dispatch to another resource. This flag > helps > + // ensure an infinite error handling loop is not entered > + boolean errorAtStart = response.isError(); > if (asyncAtStart || context.fireRequestInitEvent(request)) { > > // Ask this Context to process this request > @@ -168,29 +171,37 @@ final class StandardHostValve extends Va > context.getPipeline().getFirst().invoke(request, response); > } catch (Throwable t) { > ExceptionUtils.handleThrowable(t); > - request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); > - throwable(request, response, t); > + if (errorAtStart) { > + container.getLogger().error("Exception Processing " + > + request.getRequestURI(), t); > + } else { > + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, > t); > + throwable(request, response, t); > + } > } > > // If the request was async at the start and an error occurred > then > // the async error handling will kick-in and that will fire the > // request destroyed event *after* the error handling has taken > // place > - if (!(request.isAsync() || (asyncAtStart && request.getAttribute( > - RequestDispatcher.ERROR_EXCEPTION) != null))) { > - // Protect against NPEs if context was destroyed during a > long > - // running request. > + if (!(request.isAsync() || (asyncAtStart && > + request.getAttribute( > + RequestDispatcher.ERROR_EXCEPTION) != null))) { > + // Protect against NPEs if context was destroyed during a > + // long running request. > if (context.getState().isAvailable()) { > - // Error page processing > - response.setSuspended(false); > + if (!errorAtStart) { > + // Error page processing > + response.setSuspended(false); > > - Throwable t = (Throwable) request.getAttribute( > - RequestDispatcher.ERROR_EXCEPTION); > + Throwable t = (Throwable) request.getAttribute( > + RequestDispatcher.ERROR_EXCEPTION); > > - if (t != null) { > - throwable(request, response, t); > - } else { > - status(request, response); > + if (t != null) { > + throwable(request, response, t); > + } else { > + status(request, response); > + } > } > > context.fireRequestDestroyEvent(request); > @@ -348,7 +359,7 @@ final class StandardHostValve extends Va > * @param throwable The exception that occurred (which possibly wraps > * a root cause exception > */ > - private void throwable(Request request, Response response, > + protected void throwable(Request request, Response response, > Throwable throwable) { > Context context = request.getContext(); > if (context == null) > > Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/AsyncStateMachine.java > URL: > http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/AsyncStateMachine.java?rev=1408152&r1=1408151&r2=1408152&view=diff > ============================================================================== > --- tomcat/tc7.0.x/trunk/java/org/apache/coyote/AsyncStateMachine.java > (original) > +++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/AsyncStateMachine.java Sun > Nov 11 23:35:41 2012 > @@ -249,7 +249,8 @@ public class AsyncStateMachine<S> { > if (state == AsyncState.STARTING) { > state = AsyncState.MUST_DISPATCH; > } else if (state == AsyncState.STARTED || > - state == AsyncState.TIMING_OUT) { > + state == AsyncState.TIMING_OUT || > + state == AsyncState.ERROR) { > state = AsyncState.DISPATCHING; > doDispatch = true; > } else { > > Modified: > tomcat/tc7.0.x/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java > URL: > http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java?rev=1408152&r1=1408151&r2=1408152&view=diff > ============================================================================== > --- > tomcat/tc7.0.x/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java > (original) > +++ > tomcat/tc7.0.x/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java > Sun Nov 11 23:35:41 2012 > @@ -19,6 +19,7 @@ package org.apache.catalina.core; > > import java.io.File; > import java.io.IOException; > +import java.io.PrintWriter; > import java.util.LinkedHashMap; > import java.util.List; > import java.util.Map; > @@ -38,6 +39,8 @@ import javax.servlet.http.HttpServlet; > import javax.servlet.http.HttpServletRequest; > import javax.servlet.http.HttpServletResponse; > > +import junit.framework.Assert; > + > import static org.junit.Assert.assertEquals; > import static org.junit.Assert.assertNotNull; > import static org.junit.Assert.assertTrue; > @@ -48,6 +51,7 @@ import org.junit.Test; > import org.apache.catalina.Context; > import org.apache.catalina.Wrapper; > import org.apache.catalina.connector.Request; > +import org.apache.catalina.deploy.ErrorPage; > import org.apache.catalina.startup.Tomcat; > import org.apache.catalina.startup.TomcatBaseTest; > import org.apache.catalina.valves.TesterAccessLogValve; > @@ -174,7 +178,7 @@ public class TestAsyncContextImpl extend > assertEquals("OK-run2", bc2.toString()); > > // Check the access log > - alv.validateAccessLog(2, 200, > + alv.validateAccessLog(2, 500, > AsyncStartNoCompleteServlet.ASYNC_TIMEOUT, > AsyncStartNoCompleteServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + > REQUEST_TIME); > @@ -380,29 +384,34 @@ public class TestAsyncContextImpl extend > @Test > public void testTimeoutListenerCompleteNoDispatch() throws Exception { > // Should work > - doTestTimeout(true, null); > + doTestTimeout(Boolean.TRUE, null); > } > > @Test > public void testTimeoutListenerNoCompleteNoDispatch() throws Exception { > // Should trigger an error - must do one or other > - doTestTimeout(false, null); > + doTestTimeout(Boolean.FALSE, null); > } > > @Test > public void testTimeoutListenerCompleteDispatch() throws Exception { > // Should trigger an error - can't do both > - doTestTimeout(true, "/nonasync"); > + doTestTimeout(Boolean.TRUE, "/nonasync"); > } > > @Test > public void testTimeoutListenerNoCompleteDispatch() throws Exception { > // Should work > - doTestTimeout(false, "/nonasync"); > + doTestTimeout(Boolean.FALSE, "/nonasync"); > } > > + @Test > + public void testTimeoutNoListener() throws Exception { > + // Should work > + doTestTimeout(null, null); > + } > > - private void doTestTimeout(boolean completeOnTimeout, String dispatchUrl) > + private void doTestTimeout(Boolean completeOnTimeout, String dispatchUrl) > throws Exception { > // Setup Tomcat instance > Tomcat tomcat = getTomcatInstance(); > @@ -420,7 +429,7 @@ public class TestAsyncContextImpl extend > Context ctx = tomcat.addContext("", docBase.getAbsolutePath()); > > TimeoutServlet timeout = > - new TimeoutServlet(completeOnTimeout, dispatchUrl); > + new TimeoutServlet(completeOnTimeout, dispatchUrl); > > Wrapper wrapper = Tomcat.addServlet(ctx, "time", timeout); > wrapper.setAsyncSupported(true); > @@ -447,8 +456,11 @@ public class TestAsyncContextImpl extend > // Ignore - expected for some error conditions > } > StringBuilder expected = new StringBuilder("requestInitialized-"); > - expected.append("TimeoutServletGet-onTimeout-"); > - if (completeOnTimeout) { > + expected.append("TimeoutServletGet-"); > + if (completeOnTimeout == null) { > + expected.append("requestDestroyed"); > + } else if (completeOnTimeout.booleanValue()) { > + expected.append("onTimeout-"); > if (dispatchUrl == null) { > expected.append("onComplete-"); > expected.append("requestDestroyed"); > @@ -459,6 +471,7 @@ public class TestAsyncContextImpl extend > // never happens. > } > } else { > + expected.append("onTimeout-"); > if (dispatchUrl == null) { > expected.append("onError-"); > } else { > @@ -470,7 +483,14 @@ public class TestAsyncContextImpl extend > assertEquals(expected.toString(), res.toString()); > > // Check the access log > - if (completeOnTimeout && dispatchUrl != null) { > + if (completeOnTimeout == null) { > + alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, > + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + > + REQUEST_TIME); > + alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, > + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + > + REQUEST_TIME); > + } else if (completeOnTimeout.booleanValue() && dispatchUrl != null) { > // This error is written into Host-level AccessLogValve only > alvGlobal.validateAccessLog(1, 500, 0, > TimeoutServlet.ASYNC_TIMEOUT > + TIMEOUT_MARGIN + REQUEST_TIME); > @@ -488,12 +508,12 @@ public class TestAsyncContextImpl extend > private static class TimeoutServlet extends HttpServlet { > private static final long serialVersionUID = 1L; > > - private final boolean completeOnTimeout; > + private final Boolean completeOnTimeout; > private final String dispatchUrl; > > public static final long ASYNC_TIMEOUT = 3000; > > - public TimeoutServlet(boolean completeOnTimeout, String dispatchUrl) > { > + public TimeoutServlet(Boolean completeOnTimeout, String dispatchUrl) > { > this.completeOnTimeout = completeOnTimeout; > this.dispatchUrl = dispatchUrl; > } > @@ -506,8 +526,10 @@ public class TestAsyncContextImpl extend > final AsyncContext ac = req.startAsync(); > ac.setTimeout(ASYNC_TIMEOUT); > > - ac.addListener(new TrackingListener( > - false, completeOnTimeout, dispatchUrl)); > + if (completeOnTimeout != null) { > + ac.addListener(new TrackingListener(false, > + completeOnTimeout.booleanValue(), dispatchUrl)); > + } > } else { > resp.getWriter().print("FAIL: Async unsupported"); > } > @@ -672,7 +694,7 @@ public class TestAsyncContextImpl extend > wrapper.setAsyncSupported(true); > ctx.addServletMapping("/stage1", "tracking"); > > - TimeoutServlet timeout = new TimeoutServlet(true, null); > + TimeoutServlet timeout = new TimeoutServlet(Boolean.TRUE, null); > Wrapper wrapper2 = Tomcat.addServlet(ctx, "timeout", timeout); > wrapper2.setAsyncSupported(true); > ctx.addServletMapping("/stage2", "timeout"); > @@ -1454,4 +1476,160 @@ public class TestAsyncContextImpl extend > resp.getWriter().print("OK"); > } > } > + > + @Test > + public void testTimeoutErrorDispatchNone() throws Exception { > + doTestTimeoutErrorDispatch(null, null); > + } > + > + @Test > + public void testTimeoutErrorDispatchNonAsync() throws Exception { > + doTestTimeoutErrorDispatch(Boolean.FALSE, null); > + } > + > + @Test > + public void testTimeoutErrorDispatchAsyncStart() throws Exception { > + doTestTimeoutErrorDispatch( > + Boolean.TRUE, ErrorPageAsyncMode.NO_COMPLETE); > + } > + > + @Test > + public void testTimeoutErrorDispatchAsyncComplete() throws Exception { > + doTestTimeoutErrorDispatch(Boolean.TRUE, > ErrorPageAsyncMode.COMPLETE); > + } > + > + @Test > + public void testTimeoutErrorDispatchAsyncDispatch() throws Exception { > + doTestTimeoutErrorDispatch(Boolean.TRUE, > ErrorPageAsyncMode.DISPATCH); > + } > + > + private void doTestTimeoutErrorDispatch(Boolean asyncError, > + ErrorPageAsyncMode mode) throws Exception { > + // Setup Tomcat instance > + Tomcat tomcat = getTomcatInstance(); > + > + // Must have a real docBase - just use temp > + File docBase = new File(System.getProperty("java.io.tmpdir")); > + > + Context ctx = tomcat.addContext("", docBase.getAbsolutePath()); > + > + TimeoutServlet timeout = new TimeoutServlet(null, null); > + Wrapper w1 = Tomcat.addServlet(ctx, "time", timeout); > + w1.setAsyncSupported(true); > + ctx.addServletMapping("/async", "time"); > + > + NonAsyncServlet nonAsync = new NonAsyncServlet(); > + Wrapper w2 = Tomcat.addServlet(ctx, "nonAsync", nonAsync); > + w2.setAsyncSupported(true); > + ctx.addServletMapping("/error/nonasync", "nonAsync"); > + > + AsyncErrorPage asyncErrorPage = new AsyncErrorPage(mode); > + Wrapper w3 = Tomcat.addServlet(ctx, "asyncErrorPage", > asyncErrorPage); > + w3.setAsyncSupported(true); > + ctx.addServletMapping("/error/async", "asyncErrorPage"); > + > + if (asyncError != null) { > + ErrorPage ep = new ErrorPage(); > + ep.setErrorCode(500); > + if (asyncError.booleanValue()) { > + ep.setLocation("/error/async"); > + } else { > + ep.setLocation("/error/nonasync"); > + } > + > + ctx.addErrorPage(ep); > + } > + > + ctx.addApplicationListener(TrackingRequestListener.class.getName()); > + > + TesterAccessLogValve alv = new TesterAccessLogValve(); > + ctx.getPipeline().addValve(alv); > + TesterAccessLogValve alvGlobal = new TesterAccessLogValve(); > + tomcat.getHost().getPipeline().addValve(alvGlobal); > + > + tomcat.start(); > + ByteChunk res = new ByteChunk(); > + try { > + getUrl("http://localhost:" + getPort() + "/async", res, null); > + } catch (IOException ioe) { > + // Ignore - expected for some error conditions > + } > + > + StringBuilder expected = new StringBuilder(); > + if (asyncError == null) { > + // No error handler - just get the 500 response > + expected.append("requestInitialized-TimeoutServletGet-"); > + // Note: With an error handler the response will be reset and > these > + // will be lost > + } > + if (asyncError != null) { > + if (asyncError.booleanValue()) { > + expected.append("AsyncErrorPageGet-"); > + if (mode == ErrorPageAsyncMode.NO_COMPLETE){ > + expected.append("NoOp-"); > + } else if (mode == ErrorPageAsyncMode.COMPLETE) { > + expected.append("Complete-"); > + } else if (mode == ErrorPageAsyncMode.DISPATCH) { > + expected.append("Dispatch-NonAsyncServletGet-"); > + } > + } else { > + expected.append("NonAsyncServletGet-"); > + } > + } > + expected.append("requestDestroyed"); > + > + Assert.assertEquals(expected.toString(), res.toString()); > + > + // Check the access log > + alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, > + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + > + REQUEST_TIME); > + alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, > + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + > + REQUEST_TIME); > + } > + > + private static enum ErrorPageAsyncMode { > + NO_COMPLETE, > + COMPLETE, > + DISPATCH > + } > + > + private static class AsyncErrorPage extends HttpServlet { > + > + private static final long serialVersionUID = 1L; > + > + private final ErrorPageAsyncMode mode; > + > + public AsyncErrorPage(ErrorPageAsyncMode mode) { > + this.mode = mode; > + } > + > + @Override > + protected void doGet(HttpServletRequest req, HttpServletResponse > resp) > + throws ServletException, IOException { > + PrintWriter writer = resp.getWriter(); > + writer.write("AsyncErrorPageGet-"); > + resp.flushBuffer(); > + > + final AsyncContext ctxt = req.getAsyncContext(); > + > + switch(mode) { > + case COMPLETE: > + writer.write("Complete-"); > + ctxt.complete(); > + break; > + case DISPATCH: > + writer.write("Dispatch-"); > + ctxt.dispatch("/error/nonasync"); > + break; > + case NO_COMPLETE: > + writer.write("NoOp-"); > + break; > + default: > + // Impossible > + break; > + } > + } > + } > } > > Modified: tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml > URL: > http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml?rev=1408152&r1=1408151&r2=1408152&view=diff > ============================================================================== > --- tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml (original) > +++ tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Sun Nov 11 23:35:41 2012 > @@ -123,6 +123,11 @@ > <code>AsyncListener.complete()</code> are called with the correct > thread context class loader. (fhanik) > </fix> > + <fix> > + <bug>54123</bug>: If an asynchronous request times out without any > + <code>AsyncListener</code>s defined, a 500 error will be triggered. > + (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 > --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org