From 59588475ecb089386b378bbb91eeb4f73ee5f347 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 3 Jul 2019 12:36:55 +0200 Subject: [PATCH 01/57] Issue #3806 async sendError Avoid using isHandled as a test withing sendError as this can be called asynchronously and is in a race with the normal dispatch of the request, which could also be setting handled status. Signed-off-by: Greg Wilkins --- .../main/java/org/eclipse/jetty/server/Response.java | 10 ++++++++++ .../org/eclipse/jetty/server/handler/ErrorHandler.java | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 9a7e145fd0fe..140c5c782494 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -722,6 +722,16 @@ public boolean isWriting() return _outputType == OutputType.WRITER; } + public boolean isStreaming() + { + return _outputType == OutputType.STREAM; + } + + public boolean isWritingOrStreaming() + { + return isWriting() || isStreaming(); + } + @Override public PrintWriter getWriter() throws IOException { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index fa2bd1bf533b..00ff544d8daf 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -168,7 +168,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (baseRequest.isHandled()) + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) break; } } From 6c49a5d47f3bfd20d171fcf2b7721d1a6ead6a79 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 3 Jul 2019 14:58:15 +0200 Subject: [PATCH 02/57] Issue #3806 async sendError The ErrorHandler was dispatching directly to a context from within sendError. This meant that an async thread can call sendError and be dispatched to within the servlet container at the same time that the original thread was still dispatched to the container. This commit fixes that problem by using an async dispatch for error pages within the ErrorHandler. However, this introduces a new problem that a well behaved async app will call complete after calling sendError. Thus we have ignore complete ISEs for the remainder of the current async cycle. Signed-off-by: Greg Wilkins --- .../jetty/server/HttpChannelState.java | 49 +++++++- .../org/eclipse/jetty/server/Response.java | 3 +- .../jetty/server/handler/ErrorHandler.java | 13 +- .../eclipse/jetty/servlet/ErrorPageTest.java | 117 ++++++++++++++++++ 4 files changed, 176 insertions(+), 6 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 0b8029ab10f2..a9b37fa5df60 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -121,6 +121,7 @@ private enum AsyncRead private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; + private boolean _ignoreComplete; protected HttpChannelState(HttpChannel channel) { @@ -325,6 +326,7 @@ public void startAsync(AsyncContextEvent event) _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; + _ignoreComplete = false; } if (lastAsyncListeners != null) @@ -576,6 +578,47 @@ public void dispatch(ServletContext context, String path) scheduleDispatch(); } + public boolean asyncErrorDispatch(String path) + { + boolean dispatch = false; + AsyncContextEvent event; + try (Locker.Lock lock = _locker.lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("errorDispatch {} -> {}", toStringLocked(), path); + + if (_async != Async.STARTED) + return false; + + event = _event; + _async = Async.DISPATCH; + + if (path != null) + _event.setDispatchPath(path); + + _ignoreComplete = true; + switch (_state) + { + case DISPATCHED: + case ASYNC_IO: + case ASYNC_WOKEN: + break; + case ASYNC_WAIT: + _state = State.ASYNC_WOKEN; + dispatch = true; + break; + default: + LOG.warn("asyncErrorDispatch when complete {}", this); + break; + } + } + + cancelTimeout(event); + if (dispatch) + scheduleDispatch(); + return true; + } + protected void onTimeout() { final List listeners; @@ -676,7 +719,6 @@ public String toString() public void complete() { - // just like resume, except don't set _dispatched=true; boolean handle = false; AsyncContextEvent event; @@ -700,6 +742,11 @@ public void complete() case COMPLETE: return; default: + if (_ignoreComplete) + { + _ignoreComplete = false; + return; + } throw new IllegalStateException(this.getStatusStringLocked()); } _async = Async.COMPLETE; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 140c5c782494..d5aa292de01f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -436,6 +436,7 @@ public void sendError(int code, String message) throws IOException else _reason = message; + boolean wasAsync = request.isAsyncStarted(); // If we are allowed to have a body, then produce the error page. if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && code != SC_PARTIAL_CONTENT && code >= SC_OK) @@ -450,7 +451,7 @@ public void sendError(int code, String message) throws IOException if (errorHandler != null) errorHandler.handle(null, request, request, this); } - if (!request.isAsyncStarted()) + if (!wasAsync) closeOutput(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 00ff544d8daf..9884ae5d2761 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -85,12 +85,17 @@ public void doError(String target, Request baseRequest, HttpServletRequest reque return; } + if (_cacheControl != null) + response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); + if (this instanceof ErrorPageMapper) { String errorPage = ((ErrorPageMapper)this).getErrorPage(request); if (errorPage != null) { String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); + request.setAttribute(ERROR_PAGE, errorPage); + ServletContext servletContext = request.getServletContext(); if (servletContext == null) servletContext = ContextHandler.getCurrentContext(); @@ -102,10 +107,12 @@ else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) { LOG.warn("Error page loop {}", errorPage); } + else if (baseRequest.getHttpChannelState().asyncErrorDispatch(errorPage)) + { + return; + } else { - request.setAttribute(ERROR_PAGE, errorPage); - Dispatcher dispatcher = (Dispatcher)servletContext.getRequestDispatcher(errorPage); try { @@ -134,8 +141,6 @@ else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) } } - if (_cacheControl != null) - response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); generateAcceptableResponse(baseRequest, request, response, response.getStatus(), baseRequest.getResponse().getReason()); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index e80296a0479d..0f7fed835bd7 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -20,8 +20,19 @@ import java.io.IOException; import java.io.PrintWriter; +import java.util.EnumSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; import javax.servlet.Servlet; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -31,6 +42,7 @@ import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -59,12 +71,15 @@ public void init() throws Exception context.setContextPath("/"); + context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); + context.addServlet(DefaultServlet.class, "/"); context.addServlet(FailServlet.class, "/fail/*"); context.addServlet(FailClosedServlet.class, "/fail-closed/*"); context.addServlet(ErrorServlet.class, "/error/*"); context.addServlet(AppServlet.class, "/app/*"); context.addServlet(LongerAppServlet.class, "/longer.app/*"); + context.addServlet(AsyncSendErrorServlet.class, "/async/*"); ErrorPageErrorHandler error = new ErrorPageErrorHandler(); context.setErrorHandler(error); @@ -179,6 +194,22 @@ public void testBadMessage() throws Exception } } + @Test + public void testAsyncErrorPage() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + } + } + public static class AppServlet extends HttpServlet implements Servlet { @Override @@ -198,6 +229,37 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } + public static class AsyncSendErrorServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + final CountDownLatch hold = new CountDownLatch(1); + AsyncContext async = request.startAsync(); + async.start(() -> + { + try + { + response.sendError(599); + hold.countDown(); + async.complete(); + } + catch (IOException e) + { + Log.getLog().warn(e); + } + }); + hold.await(); + } + catch (InterruptedException e) + { + throw new ServletException(e); + } + } + } + public static class FailServlet extends HttpServlet implements Servlet { @Override @@ -246,4 +308,59 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t writer.println("getParameterMap()= " + request.getParameterMap()); } } + + public static class SingleDispatchFilter implements Filter + { + ConcurrentMap dispatches = new ConcurrentHashMap<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + final Integer key = request.hashCode(); + Thread current = Thread.currentThread(); + final Thread existing = dispatches.putIfAbsent(key, current); + if (existing != null && existing != current) + { + System.err.println("DOUBLE DISPATCH OF REQUEST!!!!!!!!!!!!!!!!!!"); + System.err.println("Thread " + existing + " :"); + for (StackTraceElement element : existing.getStackTrace()) + { + System.err.println("\tat " + element); + } + IllegalStateException ex = new IllegalStateException(); + System.err.println("Thread " + current + " :"); + for (StackTraceElement element : ex.getStackTrace()) + { + System.err.println("\tat " + element); + } + response.flushBuffer(); + throw ex; + } + + try + { + chain.doFilter(request, response); + } + finally + { + if (existing == null) + { + if (!dispatches.remove(key, current)) + throw new IllegalStateException(); + } + } + } + + @Override + public void destroy() + { + + } + } } From f3a18bcd27332809ed0e07ce309356286d0bc673 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 3 Jul 2019 17:06:57 +0200 Subject: [PATCH 03/57] Issue #3806 async sendError Fixed the closing of the output after calling sendError. Do not close if the request was async (and thus might be dispatched to an async error) or if it is now async because the error page itself is async. Signed-off-by: Greg Wilkins --- .../src/main/java/org/eclipse/jetty/server/Response.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index d5aa292de01f..3fcbb8b03621 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -451,7 +451,10 @@ public void sendError(int code, String message) throws IOException if (errorHandler != null) errorHandler.handle(null, request, request, this); } - if (!wasAsync) + + // Do not close the output if the request was async (but may not now be due to async error dispatch) + // or it is now async because the request handling is async! + if (!(wasAsync || request.isAsyncStarted())) closeOutput(); } From c6b2919ec7248d0521dd2ecf18c307f35a81f4f2 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 5 Jul 2019 09:35:15 +0200 Subject: [PATCH 04/57] updates from review Signed-off-by: Greg Wilkins --- .../jetty/server/HttpChannelState.java | 1 + .../jetty/server/handler/ErrorHandler.java | 7 ++- .../eclipse/jetty/servlet/ErrorPageTest.java | 46 ++++++++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index a9b37fa5df60..e7cd4120bb53 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -999,6 +999,7 @@ protected void recycle() _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; + _ignoreComplete = false; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 9884ae5d2761..82a49ab54a8f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -150,7 +150,7 @@ else if (baseRequest.getHttpChannelState().asyncErrorDispatch(errorPage)) * acceptable to the user-agent. The Accept header is evaluated in * quality order and the method * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)} - * is called for each mimetype until {@link Request#isHandled()} is true.

+ * is called for each mimetype until the response is written to or committed.

* * @param baseRequest The base request * @param request The servlet request (may be wrapped) @@ -232,6 +232,11 @@ protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest req *

This method is called for each mime type in the users agent's * Accept header, until {@link Request#isHandled()} is true and a * response of the appropriate type is generated. + *

+ *

The default implementation handles "text/html", "text/*" and "*/*". + * The method can be overridden to handle other types. Implementations must + * immediate produce a response and may not be async. + *

* * @param baseRequest The base request * @param request The servlet request (may be wrapped) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 0f7fed835bd7..edba5e367e1f 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -41,6 +41,7 @@ import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; @@ -195,7 +196,7 @@ public void testBadMessage() throws Exception } @Test - public void testAsyncErrorPage() throws Exception + public void testAsyncErrorPage0() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) { @@ -210,6 +211,22 @@ public void testAsyncErrorPage() throws Exception } } + @Test + public void testAsyncErrorPage1() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info?latecomplete=true HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + } + } + public static class AppServlet extends HttpServlet implements Servlet { @Override @@ -237,14 +254,33 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t try { final CountDownLatch hold = new CountDownLatch(1); + final boolean lateComplete = "true".equals(request.getParameter("latecomplete")); AsyncContext async = request.startAsync(); async.start(() -> { try { response.sendError(599); + if (!lateComplete) + async.complete(); hold.countDown(); - async.complete(); + if (lateComplete) + { + // Wait until request is recycled + while (Request.getBaseRequest(request).getMetaData()!=null) + { + try + { + System.err.println("waiting "+request); + Thread.sleep(100); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + async.complete(); + } } catch (IOException e) { @@ -334,11 +370,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha System.err.println("\tat " + element); } IllegalStateException ex = new IllegalStateException(); - System.err.println("Thread " + current + " :"); - for (StackTraceElement element : ex.getStackTrace()) - { - System.err.println("\tat " + element); - } + ex.printStackTrace(); response.flushBuffer(); throw ex; } From fe6eb479375f3c8b1debef4922c5ca0c6c559694 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sun, 7 Jul 2019 10:54:36 +0200 Subject: [PATCH 05/57] better tests Signed-off-by: Greg Wilkins --- .../jetty/server/HttpChannelState.java | 10 +++---- .../eclipse/jetty/servlet/ErrorPageTest.java | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index e7cd4120bb53..8b700cf6111b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -497,13 +497,9 @@ protected Action unhandle() return Action.WAIT; case EXPIRED: - // onTimeout handling is complete, but did not dispatch as - // we were handling. So do the error dispatch here - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - case ERRORED: + // onTimeout or onError handling is complete, but did not dispatch as + // we were handling. So do the error dispatch here _state = State.DISPATCHED; _async = Async.NOT_ASYNC; return Action.ERROR_DISPATCH; @@ -591,7 +587,7 @@ public boolean asyncErrorDispatch(String path) return false; event = _event; - _async = Async.DISPATCH; + _async = Async.DISPATCH; // TODO ERRORED????? if (path != null) _event.setDispatchPath(path); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index edba5e367e1f..6426bf2660b4 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.Filter; @@ -53,12 +54,15 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ErrorPageTest { private Server _server; private LocalConnector _connector; private StacklessLogging _stackless; + private static CountDownLatch __asyncSendErrorCompleted; + @BeforeEach public void init() throws Exception @@ -93,6 +97,8 @@ public void init() throws Exception _server.start(); _stackless = new StacklessLogging(ServletHandler.class); + + __asyncSendErrorCompleted = new CountDownLatch(1); } @AfterEach @@ -208,6 +214,7 @@ public void testAsyncErrorPage0() throws Exception assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); } } @@ -224,6 +231,7 @@ public void testAsyncErrorPage1() throws Exception assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); } } @@ -261,17 +269,16 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t try { response.sendError(599); - if (!lateComplete) - async.complete(); - hold.countDown(); + if (lateComplete) { + // Complete after original servlet + hold.countDown(); // Wait until request is recycled while (Request.getBaseRequest(request).getMetaData()!=null) { try { - System.err.println("waiting "+request); Thread.sleep(100); } catch (InterruptedException e) @@ -280,6 +287,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } async.complete(); + __asyncSendErrorCompleted.countDown(); + } + else + { + // Complete before original servlet + async.complete(); + __asyncSendErrorCompleted.countDown(); + hold.countDown(); } } catch (IOException e) @@ -332,6 +347,9 @@ public static class ErrorServlet extends HttpServlet implements Servlet @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (request.getDispatcherType()!=DispatcherType.ERROR && request.getDispatcherType()!=DispatcherType.ASYNC) + throw new IllegalStateException("Bad Dispatcher Type " + request.getDispatcherType()); + PrintWriter writer = response.getWriter(); writer.println("DISPATCH: " + request.getDispatcherType().name()); writer.println("ERROR_PAGE: " + request.getPathInfo()); From cdad73dd44a7fa9406e2be41a9f4c6ee2dd7d9d6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Tue, 9 Jul 2019 08:16:28 +0200 Subject: [PATCH 06/57] revert ignore complete Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannelState.java | 10 ---------- .../org/eclipse/jetty/server/handler/ErrorHandler.java | 6 ++++++ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 8b700cf6111b..a138ca4f4da3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -121,7 +121,6 @@ private enum AsyncRead private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; - private boolean _ignoreComplete; protected HttpChannelState(HttpChannel channel) { @@ -326,7 +325,6 @@ public void startAsync(AsyncContextEvent event) _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; - _ignoreComplete = false; } if (lastAsyncListeners != null) @@ -592,7 +590,6 @@ public boolean asyncErrorDispatch(String path) if (path != null) _event.setDispatchPath(path); - _ignoreComplete = true; switch (_state) { case DISPATCHED: @@ -715,7 +712,6 @@ public String toString() public void complete() { - // just like resume, except don't set _dispatched=true; boolean handle = false; AsyncContextEvent event; try (Locker.Lock lock = _locker.lock()) @@ -738,11 +734,6 @@ public void complete() case COMPLETE: return; default: - if (_ignoreComplete) - { - _ignoreComplete = false; - return; - } throw new IllegalStateException(this.getStatusStringLocked()); } _async = Async.COMPLETE; @@ -995,7 +986,6 @@ protected void recycle() _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; - _ignoreComplete = false; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 82a49ab54a8f..85de0b75fb1e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -107,6 +107,12 @@ else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) { LOG.warn("Error page loop {}", errorPage); } + + + // TODO handle async and sync case the same. Call the channelState to + // record there is an error dispatch needed, that will not be done until + // the current request cycle completes (or within that call if it already + // has. else if (baseRequest.getHttpChannelState().asyncErrorDispatch(errorPage)) { return; From 267d2a48a279b1b78db1a794a09c30521fd1e7c9 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 10 Jul 2019 09:48:31 +0200 Subject: [PATCH 07/57] added some TODOs Signed-off-by: Greg Wilkins --- .../src/main/java/org/eclipse/jetty/server/HttpOutput.java | 1 + .../src/main/java/org/eclipse/jetty/server/Response.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index e7dea9cba677..10842c367f78 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -200,6 +200,7 @@ public long getWritten() public void reopen() { + // TODO can we reopen from PENDING or UNREADY? _state.set(OutputState.OPEN); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 3fcbb8b03621..96126135f4fd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -398,8 +398,11 @@ public void sendError(int code, String message) throws IOException { if (LOG.isDebugEnabled()) LOG.debug("Aborting on sendError on committed response {} {}", code, message); + // TODO this is not in agreement with the sendError javadoc, which says: + // TODO * If the response has already been committed, this method throws an IllegalStateException. code = -1; } + // TODO should we also check isReady if we are async writing? If so, should we remove the WriteListener? else resetBuffer(); From 1e53ef021d69c4ad1b3da5f96e1ac37840e30a34 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 11 Jul 2019 10:49:35 +0200 Subject: [PATCH 08/57] more TODOs Signed-off-by: Greg Wilkins --- .../eclipse/jetty/server/HttpChannelState.java | 4 ++-- .../org/eclipse/jetty/server/HttpOutput.java | 4 ++++ .../java/org/eclipse/jetty/server/Response.java | 17 +++++++++-------- .../jetty/server/handler/ContextHandler.java | 1 + .../jetty/server/handler/ErrorHandler.java | 11 ++++++----- .../eclipse/jetty/servlet/ErrorPageTest.java | 12 +++++++++--- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index a138ca4f4da3..424df616e3f6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -572,7 +572,7 @@ public void dispatch(ServletContext context, String path) scheduleDispatch(); } - public boolean asyncErrorDispatch(String path) + public boolean errorDispatch(String path) { boolean dispatch = false; AsyncContextEvent event; @@ -608,7 +608,7 @@ public boolean asyncErrorDispatch(String path) cancelTimeout(event); if (dispatch) - scheduleDispatch(); + scheduleDispatch(); // TODO can we call ourselves? return true; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 10842c367f78..242f838d2f67 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -1083,6 +1083,10 @@ public void resetBuffer() if (BufferUtil.hasContent(_aggregate)) BufferUtil.clear(_aggregate); _written = 0; + + // TODO LO discard WRiteListener if sendError? + // TODO LO not reopen if sendError + reopen(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 96126135f4fd..12a9d8bfa7e1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -392,23 +392,22 @@ public void sendError(int sc) throws IOException public void sendError(int code, String message) throws IOException { if (isIncluding()) - return; + return; // TODO GW why not ISE? if (isCommitted()) { if (LOG.isDebugEnabled()) LOG.debug("Aborting on sendError on committed response {} {}", code, message); - // TODO this is not in agreement with the sendError javadoc, which says: + // TODO GW this is not in agreement with the sendError javadoc, which says: // TODO * If the response has already been committed, this method throws an IllegalStateException. code = -1; } - // TODO should we also check isReady if we are async writing? If so, should we remove the WriteListener? else - resetBuffer(); + resetBuffer(); // TODO LO should we remove the WriteListener? switch (code) { - case -1: + case -1: // TODO GW who uses -1? _channel.abort(new IOException()); return; case 102: @@ -437,7 +436,7 @@ public void sendError(int code, String message) throws IOException message = cause == null ? _reason : cause.toString(); } else - _reason = message; + _reason = message; // TODO GW why? boolean wasAsync = request.isAsyncStarted(); // If we are allowed to have a body, then produce the error page. @@ -451,14 +450,16 @@ public void sendError(int code, String message) throws IOException request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); + + // TODO somehow flag in HttpChannelState that an ERROR dispatch or page is needed if (errorHandler != null) errorHandler.handle(null, request, request, this); } // Do not close the output if the request was async (but may not now be due to async error dispatch) // or it is now async because the request handling is async! - if (!(wasAsync || request.isAsyncStarted())) - closeOutput(); + if (!(wasAsync || request.isAsyncStarted())) // TODO LO async will not longer be able to be started withing errorHandler.handle + closeOutput(); // TODO fake close so that it can be reopened later! } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 73d02c05a25c..d5fcc3a58e8c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1292,6 +1292,7 @@ public void doHandle(String target, Request baseRequest, HttpServletRequest requ if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) break; + // TODO GW How do we get here??? // We can just call doError here. If there is no error page, then one will // be generated. If there is an error page, then a RequestDispatcher will be // used to route the request through appropriate filters etc. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 85de0b75fb1e..6ec67cc4b677 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -81,7 +81,7 @@ public void doError(String target, Request baseRequest, HttpServletRequest reque String method = request.getMethod(); if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) { - baseRequest.setHandled(true); + baseRequest.setHandled(true); // TODO setHandled called within sendError.... may be called async!!! return; } @@ -103,22 +103,23 @@ public void doError(String target, Request baseRequest, HttpServletRequest reque { LOG.warn("No ServletContext for error page {}", errorPage); } - else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) + else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) // TODO maybe better loop detection. { LOG.warn("Error page loop {}", errorPage); } - // TODO handle async and sync case the same. Call the channelState to + // TODO LO handle async and sync case the same. Call the channelState to // record there is an error dispatch needed, that will not be done until // the current request cycle completes (or within that call if it already - // has. + // has). else if (baseRequest.getHttpChannelState().asyncErrorDispatch(errorPage)) { return; } else { + // TODO LO defer ERROR dispatch until after return from REQUEST dispatch. Dispatcher dispatcher = (Dispatcher)servletContext.getRequestDispatcher(errorPage); try { @@ -179,7 +180,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) // TODO revisit this fix AFTER implemented delayed dispatch break; } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 6426bf2660b4..1d789c9f1a03 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -292,9 +292,15 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t else { // Complete before original servlet - async.complete(); - __asyncSendErrorCompleted.countDown(); - hold.countDown(); + try + { + async.complete(); + __asyncSendErrorCompleted.countDown(); + } + finally + { + hold.countDown(); + } } } catch (IOException e) From 6008878a0205af4978d0503244d359b98c33cf27 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 11 Jul 2019 10:51:18 +0200 Subject: [PATCH 09/57] fixed rename Signed-off-by: Greg Wilkins --- .../main/java/org/eclipse/jetty/server/HttpChannelState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 424df616e3f6..495c2c6bb125 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -572,7 +572,7 @@ public void dispatch(ServletContext context, String path) scheduleDispatch(); } - public boolean errorDispatch(String path) + public boolean asyncErrorDispatch(String path) { boolean dispatch = false; AsyncContextEvent event; From c77f0cb75022946f4e6023ffaca493990d7e5d64 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 11 Jul 2019 11:40:11 +0200 Subject: [PATCH 10/57] cleanup ISE and more TODOs Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/Response.java | 28 +++++++++++-------- .../jetty/server/handler/ContextHandler.java | 15 +++++++--- .../eclipse/jetty/servlet/ErrorPageTest.java | 1 + 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 12a9d8bfa7e1..e3de6464115b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -388,22 +388,29 @@ public void sendError(int sc) throws IOException sendError(sc, null); } + /** + * Send an error response. + *

In addition to the servlet standard handling, this method supports some additional codes: + *

+ *
102
Send a partial PROCESSING response and allow additional responses
+ *
-1
Abort the HttpChannel and close the connection/stream
+ * + *

+ * @param code The error code + * @param message The message + * @throws IOException If an IO problem occurred sending the error response. + */ @Override public void sendError(int code, String message) throws IOException { if (isIncluding()) - return; // TODO GW why not ISE? + return; if (isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborting on sendError on committed response {} {}", code, message); - // TODO GW this is not in agreement with the sendError javadoc, which says: - // TODO * If the response has already been committed, this method throws an IllegalStateException. - code = -1; - } - else - resetBuffer(); // TODO LO should we remove the WriteListener? + throw new IllegalStateException("Committed"); + + resetBuffer(); // TODO LO should we remove the WriteListener? + _outputType = OutputType.NONE; switch (code) { @@ -417,7 +424,6 @@ public void sendError(int code, String message) throws IOException break; } - _outputType = OutputType.NONE; setContentType(null); setCharacterEncoding(null); setHeader(HttpHeader.EXPIRES, null); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index d5fcc3a58e8c..0668d2311f52 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1292,10 +1292,17 @@ public void doHandle(String target, Request baseRequest, HttpServletRequest requ if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) break; - // TODO GW How do we get here??? - // We can just call doError here. If there is no error page, then one will - // be generated. If there is an error page, then a RequestDispatcher will be - // used to route the request through appropriate filters etc. + // This is an error dispatch that didn't come via Dispatcher.error. + // This must be from an uncaught exception that is dispatching back into the + // context so that sendError can be called in scope of the context. + // + // We can just call doError here, which if not extended will end up being + // a call to sendError. + + // TODO GW why is this needed now? If sendError can be called asynchronously from outside of the scope + // TODO of a context, then why do uncaught exceptions have to be handled with a sendError call + // TODO within a context? + doError(target, baseRequest, request, response); return; default: diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 1d789c9f1a03..93de2b534217 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -49,6 +49,7 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; From d6cfc3a2da533248f63805855bc9e83a426e11cb Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 11 Jul 2019 13:44:50 +0200 Subject: [PATCH 11/57] refactored to call sendError for uncaught exceptions rather than onError Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/Dispatcher.java | 12 +------- .../org/eclipse/jetty/server/HttpChannel.java | 23 ++++++++++++++- .../org/eclipse/jetty/server/Request.java | 25 +++++++++++++++- .../org/eclipse/jetty/server/Response.java | 29 ++++++++++--------- .../jetty/server/handler/ContextHandler.java | 18 ------------ 5 files changed, 63 insertions(+), 44 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 840335af2af4..09249f955ab7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -42,8 +42,6 @@ public class Dispatcher implements RequestDispatcher { private static final Logger LOG = Log.getLogger(Dispatcher.class); - public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR"; - /** * Dispatch include attribute names */ @@ -83,15 +81,7 @@ public void forward(ServletRequest request, ServletResponse response) throws Ser public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException { - try - { - request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE); - forward(request, response, DispatcherType.ERROR); - } - finally - { - request.setAttribute(__ERROR_DISPATCH, null); - } + forward(request, response, DispatcherType.ERROR); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index e67aa1cf6261..2c9e7926f452 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -33,6 +33,7 @@ import java.util.function.Supplier; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; +import javax.servlet.UnavailableException; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpFields; @@ -58,6 +59,8 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Scheduler; +import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; + /** * HttpChannel represents a single endpoint for HTTP semantic processing. * The HttpChannel is both a HttpParser.RequestHandler, where it passively receives events from @@ -602,7 +605,25 @@ else if (noStack != null) try { - _state.onError(failure); + _request.setAttribute(ERROR_EXCEPTION, failure); + int code = HttpStatus.INTERNAL_SERVER_ERROR_500; + String reason = null; + Throwable cause = unwrap(failure, BadMessageException.class, UnavailableException.class); + if (cause instanceof BadMessageException) + { + BadMessageException bme = (BadMessageException)cause; + code = bme.getCode(); + reason = bme.getReason(); + } + else if (cause instanceof UnavailableException) + { + if (((UnavailableException)cause).isPermanent()) + code = HttpStatus.NOT_FOUND_404; + else + code = HttpStatus.SERVICE_UNAVAILABLE_503; + } + + _response.sendError(code, reason); } catch (Throwable e) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 99e2e8deafe3..2421a2e20cb1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -203,6 +203,7 @@ public static Request getBaseRequest(ServletRequest request) private String _contentType; private String _characterEncoding; private ContextHandler.Context _context; + private ContextHandler.Context _errorContext; private CookieCutter _cookies; private DispatcherType _dispatcherType; private int _inputState = INPUT_NONE; @@ -727,6 +728,22 @@ public Context getContext() return _context; } + /** + * @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous, + * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext + */ + public Context getErrorContext() + { + if (isAsyncStarted()) + { + ContextHandler handler = _channel.getState().getContextHandler(); + if (handler != null) + return handler.getServletContext(); + } + + return _errorContext; + } + /* * @see javax.servlet.http.HttpServletRequest#getContextPath() */ @@ -1917,7 +1934,13 @@ public void setContentType(String contentType) public void setContext(Context context) { _newContext = _context != context; - _context = context; + if (context == null) + _context = context; + else + { + _context = context; + _errorContext = context; + } } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index e3de6464115b..96ad7b13f99f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -444,27 +444,30 @@ public void sendError(int code, String message) throws IOException else _reason = message; // TODO GW why? - boolean wasAsync = request.isAsyncStarted(); + boolean isAsync = request.isAsyncStarted(); // If we are allowed to have a body, then produce the error page. if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && code != SC_PARTIAL_CONTENT && code >= SC_OK) { - ContextHandler.Context context = request.getContext(); - ContextHandler contextHandler = context == null ? _channel.getState().getContextHandler() : context.getContextHandler(); - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); - request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); - ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); - - // TODO somehow flag in HttpChannelState that an ERROR dispatch or page is needed - if (errorHandler != null) - errorHandler.handle(null, request, request, this); + ContextHandler.Context context = request.getErrorContext(); + if (context != null) + { + ContextHandler contextHandler = context.getContextHandler(); + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); + ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); + + // TODO somehow flag in HttpChannelState that an ERROR dispatch or page is needed + if (errorHandler != null) + errorHandler.handle(null, request, request, this); + } } // Do not close the output if the request was async (but may not now be due to async error dispatch) // or it is now async because the request handling is async! - if (!(wasAsync || request.isAsyncStarted())) // TODO LO async will not longer be able to be started withing errorHandler.handle + if (!(isAsync || request.isAsyncStarted())) // TODO LO async will not longer be able to be started withing errorHandler.handle closeOutput(); // TODO fake close so that it can be reopened later! } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 0668d2311f52..c4625cecf9e2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1287,24 +1287,6 @@ public void doHandle(String target, Request baseRequest, HttpServletRequest requ } break; - case ERROR: - // If this is already a dispatch to an error page, proceed normally - if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) - break; - - // This is an error dispatch that didn't come via Dispatcher.error. - // This must be from an uncaught exception that is dispatching back into the - // context so that sendError can be called in scope of the context. - // - // We can just call doError here, which if not extended will end up being - // a call to sendError. - - // TODO GW why is this needed now? If sendError can be called asynchronously from outside of the scope - // TODO of a context, then why do uncaught exceptions have to be handled with a sendError call - // TODO within a context? - - doError(target, baseRequest, request, response); - return; default: break; } From 1f1e6849f1ee6d334db9b5ff03bb7d25632c4fe1 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 11 Jul 2019 13:57:33 +0200 Subject: [PATCH 12/57] more of the refactor Signed-off-by: Greg Wilkins --- .../java/org/eclipse/jetty/server/handler/ErrorHandler.java | 2 +- .../src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 6ec67cc4b677..031049a0b662 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -96,7 +96,7 @@ public void doError(String target, Request baseRequest, HttpServletRequest reque String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); request.setAttribute(ERROR_PAGE, errorPage); - ServletContext servletContext = request.getServletContext(); + ContextHandler.Context servletContext = baseRequest.getErrorContext(); if (servletContext == null) servletContext = ContextHandler.getCurrentContext(); if (servletContext == null) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 93de2b534217..1d789c9f1a03 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -49,7 +49,6 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; From 53aa9fe27781b8b0005cca84110d323a2d2f76ac Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 12 Jul 2019 13:32:52 +0200 Subject: [PATCH 13/57] extra tests for sendError from completing state Signed-off-by: Greg Wilkins --- .../eclipse/jetty/servlet/ErrorPageTest.java | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 1d789c9f1a03..186391bf42b1 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -44,6 +45,7 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; @@ -69,9 +71,10 @@ public void init() throws Exception { _server = new Server(); _connector = new LocalConnector(_server); + _server.addConnector(_connector); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); - _server.addConnector(_connector); _server.setHandler(context); context.setContextPath("/"); @@ -85,6 +88,19 @@ public void init() throws Exception context.addServlet(AppServlet.class, "/app/*"); context.addServlet(LongerAppServlet.class, "/longer.app/*"); context.addServlet(AsyncSendErrorServlet.class, "/async/*"); + context.addServlet(NotEnoughServlet.class, "/notenough/*"); + + HandlerWrapper noopHandler = new HandlerWrapper() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + if (target.startsWith("/noop")) + return; + else + super.handle(target, baseRequest, request, response); + } + }; + context.insertHandler(noopHandler); ErrorPageErrorHandler error = new ErrorPageErrorHandler(); context.setErrorHandler(error); @@ -235,6 +251,44 @@ public void testAsyncErrorPage1() throws Exception } } + @Test + public void testNoop() throws Exception + { + String response = _connector.getResponse("GET /noop/info HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 404 Not Found")); + assertThat(response, Matchers.containsString("DISPATCH: ERROR")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_CODE: 404")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: null")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /noop/info")); + } + + @Test + public void testNotEnough() throws Exception + { + String response = _connector.getResponse("GET /notenough/info HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 500 insufficient content written")); + assertThat(response, Matchers.containsString("DISPATCH: ERROR")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_CODE: 404")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: null")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /notenough/info")); + } + + @Test + public void testNotEnoughCommitted() throws Exception + { + String response = _connector.getResponse("GET /notenough/info?commit=true HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); + assertThat(response, Matchers.containsString("Content-Length: 1000")); + assertThat(response, Matchers.endsWith("SomeBytes")); + } + + public static class AppServlet extends HttpServlet implements Servlet { @Override @@ -348,6 +402,18 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } + public static class NotEnoughServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(1000); + response.getOutputStream().write("SomeBytes".getBytes(StandardCharsets.UTF_8)); + if (Boolean.parseBoolean(request.getParameter("commit"))) + response.flushBuffer(); + } + } + public static class ErrorServlet extends HttpServlet implements Servlet { @Override From d03496cf5d02f77e20f8231a41aa1365cfc758a3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 17 Jul 2019 08:35:56 +0200 Subject: [PATCH 14/57] Issue #3806 async sendError Reworked HttpChannelState and sendError so that sendError is now just a change of state. All the work is done in the ErrorDispatch action, including calling the ErrorHandler. Async not yet working. Signed-off-by: Greg Wilkins --- .../jetty/client/ConnectionPoolTest.java | 2 +- .../org/eclipse/jetty/http/HttpGenerator.java | 11 +- .../org/eclipse/jetty/http/HttpMethod.java | 10 -- .../org/eclipse/jetty/http/HttpStatus.java | 14 ++ .../org/eclipse/jetty/server/HttpChannel.java | 168 +++++++++--------- .../jetty/server/HttpChannelState.java | 114 ++++-------- .../eclipse/jetty/server/HttpConnection.java | 2 +- .../org/eclipse/jetty/server/HttpOutput.java | 70 +++++--- .../org/eclipse/jetty/server/Response.java | 73 ++------ .../jetty/server/handler/ErrorHandler.java | 90 ++-------- .../eclipse/jetty/servlet/ErrorPageTest.java | 108 +++++++++-- 11 files changed, 313 insertions(+), 349 deletions(-) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index 79942ba172b5..d099a733d286 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -120,7 +120,7 @@ public void test(Class connectionPoolClass, Connection @Override protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - switch (HttpMethod.fromString(request.getMethod())) + switch (HttpMethod.valueOf(request.getMethod())) { case GET: { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index e23bb1bad04c..dec9094b7665 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -51,13 +51,12 @@ public class HttpGenerator private static final byte[] __colon_space = new byte[]{':', ' '}; public static final MetaData.Response CONTINUE_100_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 100, null, null, -1); public static final MetaData.Response PROGRESS_102_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 102, null, null, -1); - public static final MetaData.Response RESPONSE_500_INFO = - new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields() + public static final MetaData.Response RESPONSE_500_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields() + { { - { - put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); - } - }, 0); + put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); + } + }, 0); // states public enum State diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java index 741be954df55..930035d5092d 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java @@ -180,14 +180,4 @@ public String asString() return toString(); } - /** - * Converts the given String parameter to an HttpMethod - * - * @param method the String to get the equivalent HttpMethod from - * @return the HttpMethod or null if the parameter method is unknown - */ - public static HttpMethod fromString(String method) - { - return CACHE.get(method); - } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 887bb6362593..20e6f8cf431f 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -320,6 +320,20 @@ public static String getMessage(int code) } } + public static boolean hasNoBody(int status) + { + switch (status) + { + case NO_CONTENT_204: + case NOT_MODIFIED_304: + case PARTIAL_CONTENT_206: + return true; + + default: + return status < OK_200; + } + } + /** * Simple test against an code to determine if it falls into the * Informational message category as defined in the write test Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; - _response.setStatus(code); _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); + _response.setStatus(code); + _request.setHandled(false); _response.getHttpOutput().reopen(); - try + ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); + ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); + + // If we can't have a body, then create a minimal error response. + if (HttpStatus.hasNoBody(code) || + errorHandler == null || + !errorHandler.errorPageForMethod(_request.getMethod())) { - _request.setDispatcherType(DispatcherType.ERROR); - notifyBeforeDispatch(_request); - getServer().handle(this); + minimalErrorResponse(code); + _request.setHandled(true); + break; } - catch (Throwable x) + + // Look for an error page + String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; + Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; + + if (errorDispatcher != null) { - notifyDispatchFailure(_request, x); - throw x; + try + { + _request.setAttribute(ErrorHandler.ERROR_PAGE, errorPage); + _request.setDispatcherType(DispatcherType.ERROR); + notifyBeforeDispatch(_request); + errorDispatcher.error(_request, _response); + break; + } + catch (Throwable x) + { + notifyDispatchFailure(_request, x); + throw x; + } + finally + { + notifyAfterDispatch(_request); + _request.setDispatcherType(null); + } } - finally + else { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); + // Allow ErrorHandler to generate response + errorHandler.handle(null, _request, _request, _response); + _request.setHandled(true); } } catch (Throwable x) @@ -448,15 +479,15 @@ public boolean handle() LOG.debug("Could not perform ERROR dispatch, aborting", x); Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); if (failure == null) - { - minimalErrorResponse(x); - } + failure = x; else - { - if (x != failure) - failure.addSuppressed(x); - minimalErrorResponse(failure); - } + failure.addSuppressed(x); + minimalErrorResponse(failure); + } + finally + { + // clean up the context that was set in Response.sendError + _request.removeAttribute(ErrorHandler.ERROR_CONTEXT); } break; } @@ -494,39 +525,26 @@ public boolean handle() case COMPLETE: { - try + if (!_response.isCommitted() && !_request.isHandled()) { - if (!_response.isCommitted() && !_request.isHandled()) - { - _response.sendError(HttpStatus.NOT_FOUND_404); - } + _response.sendError(HttpStatus.NOT_FOUND_404); + break; + } + + // RFC 7230, section 3.3. + if (!_response.isContentComplete(_response.getHttpOutput().getWritten())) + { + if (isCommitted()) + abort(new IOException("insufficient content written")); else { - // RFC 7230, section 3.3. - int status = _response.getStatus(); - boolean hasContent = !(_request.isHead() || - HttpMethod.CONNECT.is(_request.getMethod()) && status == HttpStatus.OK_200 || - HttpStatus.isInformational(status) || - status == HttpStatus.NO_CONTENT_204 || - status == HttpStatus.NOT_MODIFIED_304); - if (hasContent && !_response.isContentComplete(_response.getHttpOutput().getWritten())) - { - if (isCommitted()) - abort(new IOException("insufficient content written")); - else - _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "insufficient content written"); - } + _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "insufficient content written"); + break; } - _response.closeOutput(); } - finally - { - _request.setHandled(true); - _state.onComplete(); - onCompleted(); - } - - break loop; + _response.closeOutput(); + _state.completed(); + break; } default: @@ -603,36 +621,7 @@ else if (noStack != null) LOG.warn(_request.getRequestURI(), failure); } - try - { - _request.setAttribute(ERROR_EXCEPTION, failure); - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String reason = null; - Throwable cause = unwrap(failure, BadMessageException.class, UnavailableException.class); - if (cause instanceof BadMessageException) - { - BadMessageException bme = (BadMessageException)cause; - code = bme.getCode(); - reason = bme.getReason(); - } - else if (cause instanceof UnavailableException) - { - if (((UnavailableException)cause).isPermanent()) - code = HttpStatus.NOT_FOUND_404; - else - code = HttpStatus.SERVICE_UNAVAILABLE_503; - } - - _response.sendError(code, reason); - } - catch (Throwable e) - { - if (e != failure) - failure.addSuppressed(e); - LOG.warn("ERROR dispatch failed", failure); - // Try to send a minimal response. - minimalErrorResponse(failure); - } + _state.onError(failure); } /** @@ -656,6 +645,21 @@ protected Throwable unwrap(Throwable failure, Class... targets) return null; } + private void minimalErrorResponse(int code) + { + try + { + _response.reset(true); + _response.setStatus(code); + _response.flushBuffer(); + _request.setHandled(true); + } + catch (Throwable x) + { + abort(x); + } + } + private void minimalErrorResponse(Throwable failure) { try diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 495c2c6bb125..4bf16dcab732 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -23,7 +23,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncListener; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; @@ -32,6 +31,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Locker; @@ -40,6 +40,8 @@ import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE; import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; +import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI; +import static javax.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE; /** @@ -58,7 +60,7 @@ public enum State { IDLE, // Idle request DISPATCHED, // Request dispatched to filter/servlet - THROWN, // Exception thrown while DISPATCHED + ERRORED, // Error reported while DISPATCHED ASYNC_WAIT, // Suspended and waiting ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT ASYNC_IO, // Dispatched for async IO @@ -421,7 +423,7 @@ protected Action unhandle() case COMPLETED: return Action.TERMINATED; - case THROWN: + case ERRORED: _state = State.DISPATCHED; return Action.ERROR_DISPATCH; @@ -572,46 +574,6 @@ public void dispatch(ServletContext context, String path) scheduleDispatch(); } - public boolean asyncErrorDispatch(String path) - { - boolean dispatch = false; - AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("errorDispatch {} -> {}", toStringLocked(), path); - - if (_async != Async.STARTED) - return false; - - event = _event; - _async = Async.DISPATCH; // TODO ERRORED????? - - if (path != null) - _event.setDispatchPath(path); - - switch (_state) - { - case DISPATCHED: - case ASYNC_IO: - case ASYNC_WOKEN: - break; - case ASYNC_WAIT: - _state = State.ASYNC_WOKEN; - dispatch = true; - break; - default: - LOG.warn("asyncErrorDispatch when complete {}", this); - break; - } - } - - cancelTimeout(event); - if (dispatch) - scheduleDispatch(); // TODO can we call ourselves? - return true; - } - protected void onTimeout() { final List listeners; @@ -767,18 +729,15 @@ public void errorComplete() protected void onError(Throwable th) { - final List listeners; - final AsyncContextEvent event; - final Request baseRequest = _channel.getRequest(); - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String reason = null; + String message = null; Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); + if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; code = bme.getCode(); - reason = bme.getReason(); + message = bme.getReason(); } else if (cause instanceof UnavailableException) { @@ -788,41 +747,43 @@ else if (cause instanceof UnavailableException) code = HttpStatus.SERVICE_UNAVAILABLE_503; } + onError(th, code, message); + } + + public void onError(Throwable cause, int code, String message) + { + final List listeners; + final AsyncContextEvent event; + final Request baseRequest = _channel.getRequest(); + + if (message == null) + message = cause == null ? HttpStatus.getMessage(code) : cause.toString(); + + // we are allowed to have a body, then produce the error page. + ContextHandler.Context context = baseRequest.getErrorContext(); + if (context != null) + baseRequest.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + baseRequest.setAttribute(ERROR_REQUEST_URI, baseRequest.getRequestURI()); + baseRequest.setAttribute(ERROR_SERVLET_NAME, baseRequest.getServletName()); + baseRequest.setAttribute(ERROR_STATUS_CODE, code); + baseRequest.setAttribute(ERROR_EXCEPTION, cause); + baseRequest.setAttribute(ERROR_EXCEPTION_TYPE, cause == null ? null : cause.getClass()); + baseRequest.setAttribute(ERROR_MESSAGE, message); + try (Locker.Lock lock = _locker.lock()) { if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), th); + LOG.debug("onError {} {}", toStringLocked(), cause); // Set error on request. if (_event != null) - { - _event.addThrowable(th); - _event.getSuppliedRequest().setAttribute(ERROR_STATUS_CODE, code); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION, th); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - _event.getSuppliedRequest().setAttribute(ERROR_MESSAGE, reason); - } - else - { - Throwable error = (Throwable)baseRequest.getAttribute(ERROR_EXCEPTION); - if (error != null) - throw new IllegalStateException("Error already set", error); - baseRequest.setAttribute(ERROR_STATUS_CODE, code); - baseRequest.setAttribute(ERROR_EXCEPTION, th); - baseRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - baseRequest.setAttribute(ERROR_MESSAGE, reason); - } + _event.addThrowable(cause); // Are we blocking? if (_async == Async.NOT_ASYNC) { - // Only called from within HttpChannel Handling, so much be dispatched, let's stay dispatched! - if (_state == State.DISPATCHED) - { - _state = State.THROWN; - return; - } - throw new IllegalStateException(this.getStatusStringLocked()); + _state = State.ERRORED; + return; } // We are Async @@ -900,7 +861,7 @@ public String toString() } } - protected void onComplete() + protected void completed() { final List aListeners; final AsyncContextEvent event; @@ -920,6 +881,9 @@ protected void onComplete() break; default: + System.err.println(this.getStatusStringLocked()); + new Throwable().printStackTrace(); + System.exit(1); throw new IllegalStateException(this.getStatusStringLocked()); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index d81963c40588..7aa1dd3ae229 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -283,7 +283,7 @@ else if (filled < 0) case COMPLETING: case COMPLETED: case IDLE: - case THROWN: + case ERRORED: case ASYNC_ERROR: getEndPoint().shutdownOutput(); break; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 242f838d2f67..c01e62f0500a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -200,7 +200,6 @@ public long getWritten() public void reopen() { - // TODO can we reopen from PENDING or UNREADY? _state.set(OutputState.OPEN); } @@ -257,6 +256,26 @@ private void abort(Throwable failure) _channel.abort(failure); } + public void sendErrorClose() + { + while (true) + { + OutputState state = _state.get(); + switch (state) + { + case OPEN: + case READY: + case ASYNC: + if (!_state.compareAndSet(state, OutputState.CLOSED)) + continue; + return; + + default: + throw new IllegalStateException(state.toString()); + } + } + } + @Override public void close() { @@ -271,6 +290,7 @@ public void close() } case ASYNC: { + // TODO review this logic? // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. // However it is desirable to allow a close at any time, specially if @@ -381,7 +401,13 @@ private void releaseBuffer() public boolean isClosed() { - return _state.get() == OutputState.CLOSED; + switch (_state.get()) + { + case CLOSED: + return true; + default: + return false; + } } public boolean isAsync() @@ -403,7 +429,8 @@ public void flush() throws IOException { while (true) { - switch (_state.get()) + OutputState state = _state.get(); + switch (state) { case OPEN: write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false); @@ -413,7 +440,7 @@ public void flush() throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, OutputState.PENDING)) continue; new AsyncFlush().iterate(); return; @@ -431,7 +458,7 @@ public void flush() throws IOException return; default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } } } @@ -442,7 +469,8 @@ public void write(byte[] b, int off, int len) throws IOException // Async or Blocking ? while (true) { - switch (_state.get()) + OutputState state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -452,7 +480,7 @@ public void write(byte[] b, int off, int len) throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, OutputState.PENDING)) continue; // Should we aggregate? @@ -469,7 +497,7 @@ public void write(byte[] b, int off, int len) throws IOException if (filled == len && !BufferUtil.isFull(_aggregate)) { if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) - throw new IllegalStateException(); + throw new IllegalStateException(_state.get().toString()); return; } @@ -493,7 +521,7 @@ public void write(byte[] b, int off, int len) throws IOException throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -567,7 +595,8 @@ public void write(ByteBuffer buffer) throws IOException // Async or Blocking ? while (true) { - switch (_state.get()) + OutputState state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -577,7 +606,7 @@ public void write(ByteBuffer buffer) throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, OutputState.PENDING)) continue; // Do the asynchronous writing from the callback @@ -596,7 +625,7 @@ public void write(ByteBuffer buffer) throws IOException throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -1083,11 +1112,6 @@ public void resetBuffer() if (BufferUtil.hasContent(_aggregate)) BufferUtil.clear(_aggregate); _written = 0; - - // TODO LO discard WRiteListener if sendError? - // TODO LO not reopen if sendError - - reopen(); } @Override @@ -1114,6 +1138,9 @@ public boolean isReady() switch (_state.get()) { case OPEN: + case READY: + case ERROR: + case CLOSED: return true; case ASYNC: @@ -1121,9 +1148,6 @@ public boolean isReady() continue; return true; - case READY: - return true; - case PENDING: if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) continue; @@ -1132,12 +1156,6 @@ public boolean isReady() case UNREADY: return false; - case ERROR: - return true; - - case CLOSED: - return true; - default: throw new IllegalStateException(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 96ad7b13f99f..7f2c405f63a5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -29,7 +29,6 @@ import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @@ -53,8 +52,6 @@ import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; @@ -406,69 +403,22 @@ public void sendError(int code, String message) throws IOException if (isIncluding()) return; - if (isCommitted()) - throw new IllegalStateException("Committed"); + reset(true); - resetBuffer(); // TODO LO should we remove the WriteListener? - _outputType = OutputType.NONE; + _out.sendErrorClose(); switch (code) { - case -1: // TODO GW who uses -1? - _channel.abort(new IOException()); - return; + case -1: + _channel.abort(new IOException(message)); + break; case 102: sendProcessing(); - return; + break; default: + _channel.getState().onError(null, code, message); break; } - - setContentType(null); - setCharacterEncoding(null); - setHeader(HttpHeader.EXPIRES, null); - setHeader(HttpHeader.LAST_MODIFIED, null); - setHeader(HttpHeader.CACHE_CONTROL, null); - setHeader(HttpHeader.CONTENT_TYPE, null); - setHeader(HttpHeader.CONTENT_LENGTH, null); - - setStatus(code); - - Request request = _channel.getRequest(); - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - if (message == null) - { - _reason = HttpStatus.getMessage(code); - message = cause == null ? _reason : cause.toString(); - } - else - _reason = message; // TODO GW why? - - boolean isAsync = request.isAsyncStarted(); - // If we are allowed to have a body, then produce the error page. - if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && - code != SC_PARTIAL_CONTENT && code >= SC_OK) - { - ContextHandler.Context context = request.getErrorContext(); - if (context != null) - { - ContextHandler contextHandler = context.getContextHandler(); - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); - request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); - ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); - - // TODO somehow flag in HttpChannelState that an ERROR dispatch or page is needed - if (errorHandler != null) - errorHandler.handle(null, request, request, this); - } - } - - // Do not close the output if the request was async (but may not now be due to async error dispatch) - // or it is now async because the request handling is async! - if (!(isAsync || request.isAsyncStarted())) // TODO LO async will not longer be able to be started withing errorHandler.handle - closeOutput(); // TODO fake close so that it can be reopened later! } /** @@ -1062,14 +1012,20 @@ public void flushBuffer() throws IOException public void reset() { reset(false); + _out.reopen(); } public void reset(boolean preserveCookies) { - resetForForward(); + _out.resetBuffer(); + _outputType = OutputType.NONE; _status = 200; _reason = null; _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; List cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; _fields.clear(); @@ -1125,6 +1081,7 @@ public void resetForForward() public void resetBuffer() { _out.resetBuffer(); + _out.reopen(); } public void setTrailers(Supplier trailers) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 031049a0b662..6fc50beeffa4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -27,18 +27,14 @@ import java.nio.charset.StandardCharsets; import java.util.List; import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.QuotedQualityCSV; -import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; @@ -57,6 +53,7 @@ public class ErrorHandler extends AbstractHandler { private static final Logger LOG = Log.getLogger(ErrorHandler.class); public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; + public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; boolean _showStacks = true; boolean _showMessageInTitle = true; @@ -66,6 +63,19 @@ public ErrorHandler() { } + public boolean errorPageForMethod(String method) + { + switch (method) + { + case "GET": + case "POST": + case "HEAD": + return true; + default: + return false; + } + } + /* * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @@ -78,75 +88,9 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques @Override public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - String method = request.getMethod(); - if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) - { - baseRequest.setHandled(true); // TODO setHandled called within sendError.... may be called async!!! - return; - } - - if (_cacheControl != null) - response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); - - if (this instanceof ErrorPageMapper) - { - String errorPage = ((ErrorPageMapper)this).getErrorPage(request); - if (errorPage != null) - { - String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); - request.setAttribute(ERROR_PAGE, errorPage); - - ContextHandler.Context servletContext = baseRequest.getErrorContext(); - if (servletContext == null) - servletContext = ContextHandler.getCurrentContext(); - if (servletContext == null) - { - LOG.warn("No ServletContext for error page {}", errorPage); - } - else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) // TODO maybe better loop detection. - { - LOG.warn("Error page loop {}", errorPage); - } - - - // TODO LO handle async and sync case the same. Call the channelState to - // record there is an error dispatch needed, that will not be done until - // the current request cycle completes (or within that call if it already - // has). - else if (baseRequest.getHttpChannelState().asyncErrorDispatch(errorPage)) - { - return; - } - else - { - // TODO LO defer ERROR dispatch until after return from REQUEST dispatch. - Dispatcher dispatcher = (Dispatcher)servletContext.getRequestDispatcher(errorPage); - try - { - if (LOG.isDebugEnabled()) - LOG.debug("error page dispatch {}->{}", errorPage, dispatcher); - if (dispatcher != null) - { - dispatcher.error(request, response); - return; - } - LOG.warn("No error page found " + errorPage); - } - catch (ServletException e) - { - LOG.warn(Log.EXCEPTION, e); - return; - } - } - } - else - { - if (LOG.isDebugEnabled()) - { - LOG.debug("No Error Page mapping for request({} {}) (using default)", request.getMethod(), request.getRequestURI()); - } - } - } + String cacheControl = getCacheControl(); + if (cacheControl != null) + response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl); generateAcceptableResponse(baseRequest, request, response, response.getStatus(), baseRequest.getResponse().getReason()); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 186391bf42b1..e1434f472da6 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.Filter; @@ -51,6 +52,7 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -64,7 +66,7 @@ public class ErrorPageTest private LocalConnector _connector; private StacklessLogging _stackless; private static CountDownLatch __asyncSendErrorCompleted; - + private ErrorPageErrorHandler _errorPageErrorHandler; @BeforeEach public void init() throws Exception @@ -87,10 +89,12 @@ public void init() throws Exception context.addServlet(ErrorServlet.class, "/error/*"); context.addServlet(AppServlet.class, "/app/*"); context.addServlet(LongerAppServlet.class, "/longer.app/*"); + context.addServlet(SyncSendErrorServlet.class, "/sync/*"); context.addServlet(AsyncSendErrorServlet.class, "/async/*"); context.addServlet(NotEnoughServlet.class, "/notenough/*"); - HandlerWrapper noopHandler = new HandlerWrapper() { + HandlerWrapper noopHandler = new HandlerWrapper() + { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { @@ -102,14 +106,15 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques }; context.insertHandler(noopHandler); - ErrorPageErrorHandler error = new ErrorPageErrorHandler(); - context.setErrorHandler(error); - error.addErrorPage(599, "/error/599"); - error.addErrorPage(400, "/error/400"); + _errorPageErrorHandler = new ErrorPageErrorHandler(); + context.setErrorHandler(_errorPageErrorHandler); + _errorPageErrorHandler.addErrorPage(597, "/sync"); + _errorPageErrorHandler.addErrorPage(599, "/error/599"); + _errorPageErrorHandler.addErrorPage(400, "/error/400"); // error.addErrorPage(500,"/error/500"); - error.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException"); - error.addErrorPage(BadMessageException.class, "/error/BadMessageException"); - error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage"); + _errorPageErrorHandler.addErrorPage(IllegalStateException.class.getCanonicalName(), "/error/TestException"); + _errorPageErrorHandler.addErrorPage(BadMessageException.class, "/error/BadMessageException"); + _errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/error/GlobalErrorPage"); _server.start(); _stackless = new StacklessLogging(ServletHandler.class); @@ -125,6 +130,57 @@ public void destroy() throws Exception _server.join(); } + @Test + void testGenerateAcceptableResponse_noAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, Matchers.containsString("Error 598 598")); + assertThat(response, Matchers.containsString("

HTTP ERROR 598

")); + assertThat(response, Matchers.containsString("Problem accessing /fail/code. Reason:")); + } + + @Test + void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + // even when text/html is not the 1st content type, a html error page should still be generated + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + + "Accept: application/bytes,text/html\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, Matchers.containsString("Error 598 598")); + assertThat(response, Matchers.containsString("

HTTP ERROR 598

")); + assertThat(response, Matchers.containsString("Problem accessing /fail/code. Reason:")); + } + + @Test + void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception + { + // no global error page here + _errorPageErrorHandler.getErrorPages().remove(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE); + + String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + + "Accept: application/bytes\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); + assertThat(response, not(Matchers.containsString("Error 598 598"))); + assertThat(response, not(Matchers.containsString("

HTTP ERROR 598

"))); + assertThat(response, not(Matchers.containsString("Problem accessing /fail/code. Reason:"))); + } + + @Test + void testNestedSendErrorDoesNotLoop() throws Exception + { + String response = _connector.getResponse("GET /fail/code?code=597 HTTP/1.0\r\n\r\n"); + System.out.println(response); + assertThat(response, Matchers.containsString("HTTP/1.1 597 597")); + assertThat(response, not(Matchers.containsString("time this error page is being accessed"))); + } + @Test public void testSendErrorClosedResponse() throws Exception { @@ -205,7 +261,7 @@ public void testBadMessage() throws Exception try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) { String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n"); - assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad query encoding")); + assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad Request")); assertThat(response, Matchers.containsString("ERROR_PAGE: /BadMessageException")); assertThat(response, Matchers.containsString("ERROR_MESSAGE: Bad query encoding")); assertThat(response, Matchers.containsString("ERROR_CODE: 400")); @@ -218,6 +274,7 @@ public void testBadMessage() throws Exception } @Test + @Disabled public void testAsyncErrorPage0() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) @@ -234,7 +291,9 @@ public void testAsyncErrorPage0() throws Exception } } + // TODO re-enable once async is implemented @Test + @Disabled public void testAsyncErrorPage1() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) @@ -261,7 +320,7 @@ public void testNoop() throws Exception assertThat(response, Matchers.containsString("ERROR_CODE: 404")); assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); - assertThat(response, Matchers.containsString("ERROR_SERVLET: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.DefaultServlet-")); assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /noop/info")); } @@ -269,13 +328,13 @@ public void testNoop() throws Exception public void testNotEnough() throws Exception { String response = _connector.getResponse("GET /notenough/info HTTP/1.0\r\n\r\n"); - assertThat(response, Matchers.containsString("HTTP/1.1 500 insufficient content written")); + assertThat(response, Matchers.containsString("HTTP/1.1 500 Server Error")); assertThat(response, Matchers.containsString("DISPATCH: ERROR")); assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); - assertThat(response, Matchers.containsString("ERROR_CODE: 404")); + assertThat(response, Matchers.containsString("ERROR_CODE: 500")); assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); - assertThat(response, Matchers.containsString("ERROR_SERVLET: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$NotEnoughServlet-")); assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /notenough/info")); } @@ -288,7 +347,6 @@ public void testNotEnoughCommitted() throws Exception assertThat(response, Matchers.endsWith("SomeBytes")); } - public static class AppServlet extends HttpServlet implements Servlet { @Override @@ -308,6 +366,21 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } + public static class SyncSendErrorServlet extends HttpServlet implements Servlet + { + public static final AtomicInteger COUNTER = new AtomicInteger(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + int count = COUNTER.incrementAndGet(); + + PrintWriter writer = response.getWriter(); + writer.println("this is the " + count + " time this error page is being accessed"); + response.sendError(597, "loop #" + count); + } + } + public static class AsyncSendErrorServlet extends HttpServlet implements Servlet { @Override @@ -329,7 +402,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t // Complete after original servlet hold.countDown(); // Wait until request is recycled - while (Request.getBaseRequest(request).getMetaData()!=null) + while (Request.getBaseRequest(request).getMetaData() != null) { try { @@ -398,6 +471,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } catch (Throwable ignore) { + Log.getLog().ignore(ignore); } } } @@ -419,7 +493,7 @@ public static class ErrorServlet extends HttpServlet implements Servlet @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (request.getDispatcherType()!=DispatcherType.ERROR && request.getDispatcherType()!=DispatcherType.ASYNC) + if (request.getDispatcherType() != DispatcherType.ERROR && request.getDispatcherType() != DispatcherType.ASYNC) throw new IllegalStateException("Bad Dispatcher Type " + request.getDispatcherType()); PrintWriter writer = response.getWriter(); From ae71b99a6c04e8282ca86542bb23e19f8f0a665f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 17 Jul 2019 08:55:53 +0200 Subject: [PATCH 15/57] Issue #3806 async sendError Additional tests Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 1 - .../eclipse/jetty/servlet/ErrorPageTest.java | 71 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 97293a64d409..10fa32bc3459 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -419,7 +419,6 @@ public boolean handle() { // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError - // todo -> write test Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index e1434f472da6..156dc5dc1c7c 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -92,6 +92,8 @@ public void init() throws Exception context.addServlet(SyncSendErrorServlet.class, "/sync/*"); context.addServlet(AsyncSendErrorServlet.class, "/async/*"); context.addServlet(NotEnoughServlet.class, "/notenough/*"); + context.addServlet(DeleteServlet.class, "/delete/*"); + context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*"); HandlerWrapper noopHandler = new HandlerWrapper() { @@ -108,6 +110,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques _errorPageErrorHandler = new ErrorPageErrorHandler(); context.setErrorHandler(_errorPageErrorHandler); + _errorPageErrorHandler.addErrorPage(595, "/error/595"); _errorPageErrorHandler.addErrorPage(597, "/sync"); _errorPageErrorHandler.addErrorPage(599, "/error/599"); _errorPageErrorHandler.addErrorPage(400, "/error/400"); @@ -130,6 +133,52 @@ public void destroy() throws Exception _server.join(); } + @Test + void testErrorOverridesStatus() throws Exception + { + String response = _connector.getResponse("GET /error-and-status/anything HTTP/1.0\r\n\r\n"); + System.err.println(response); + assertThat(response, Matchers.containsString("HTTP/1.1 594 594")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); + assertThat(response, Matchers.containsString("ERROR_MESSAGE: custom get error")); + assertThat(response, Matchers.containsString("ERROR_CODE: 594")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$ErrorAndStatusServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /error-and-status/anything")); + } + + @Test + void testHttp204CannotHaveBody() throws Exception + { + String response = _connector.getResponse("GET /fail/code?code=204 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 204 No Content")); + assertThat(response, not(Matchers.containsString("DISPATCH: "))); + assertThat(response, not(Matchers.containsString("ERROR_PAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_CODE: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: "))); + assertThat(response, not(Matchers.containsString("ERROR_SERVLET: "))); + assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: "))); + } + + @Test + void testDeleteCannotHaveBody() throws Exception + { + String response = _connector.getResponse("DELETE /delete/anything HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 595 595")); + assertThat(response, not(Matchers.containsString("DISPATCH: "))); + assertThat(response, not(Matchers.containsString("ERROR_PAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_MESSAGE: "))); + assertThat(response, not(Matchers.containsString("ERROR_CODE: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION: "))); + assertThat(response, not(Matchers.containsString("ERROR_EXCEPTION_TYPE: "))); + assertThat(response, not(Matchers.containsString("ERROR_SERVLET: "))); + assertThat(response, not(Matchers.containsString("ERROR_REQUEST_URI: "))); + + assertThat(response, not(containsString("This shouldn't be seen"))); + } + @Test void testGenerateAcceptableResponse_noAcceptHeader() throws Exception { @@ -476,6 +525,26 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } + public static class ErrorAndStatusServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(594, "custom get error"); + response.setStatus(200); + } + } + + public static class DeleteServlet extends HttpServlet implements Servlet + { + @Override + protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.getWriter().append("This shouldn't be seen"); + response.sendError(595, "custom delete"); + } + } + public static class NotEnoughServlet extends HttpServlet implements Servlet { @Override @@ -491,7 +560,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t public static class ErrorServlet extends HttpServlet implements Servlet { @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getDispatcherType() != DispatcherType.ERROR && request.getDispatcherType() != DispatcherType.ASYNC) throw new IllegalStateException("Bad Dispatcher Type " + request.getDispatcherType()); From 72ffe697521fe308846d5045b7a1838bfdd0398b Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 17 Jul 2019 11:18:36 +0200 Subject: [PATCH 16/57] Issue #3806 async sendError Converted ERRORED state to a separate boolean so it can be used for both Sync and Async dispatches. Removed ASYNC_IO state as it was just the same as DISPATCHED The async onError listener handling is now most likely broken. Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 2 +- .../jetty/server/HttpChannelState.java | 218 ++++++++++-------- .../eclipse/jetty/server/HttpConnection.java | 1 - .../org/eclipse/jetty/server/Response.java | 2 +- .../eclipse/jetty/servlet/ErrorPageTest.java | 13 +- 5 files changed, 131 insertions(+), 105 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 10fa32bc3459..139dcbf9abce 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -620,7 +620,7 @@ else if (noStack != null) LOG.warn(_request.getRequestURI(), failure); } - _state.onError(failure); + _state.thrownError(failure); } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 4bf16dcab732..ab90ce3be54d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -59,11 +59,9 @@ public class HttpChannelState public enum State { IDLE, // Idle request - DISPATCHED, // Request dispatched to filter/servlet - ERRORED, // Error reported while DISPATCHED + DISPATCHED, // Request dispatched to filter/servlet or Async IO callback ASYNC_WAIT, // Suspended and waiting ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT - ASYNC_IO, // Dispatched for async IO ASYNC_ERROR, // Async error from ASYNC_WAIT COMPLETING, // Response is completable COMPLETED, // Response is completed @@ -113,6 +111,8 @@ private enum AsyncRead READY // isReady() was false, onContentAdded has been called } + private static final Throwable SEND_ERROR_CAUSE = new Throwable("SEND_ERROR_CAUSE"); + private final Locker _locker = new Locker(); private final HttpChannel _channel; private List _asyncListeners; @@ -123,6 +123,7 @@ private enum AsyncRead private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; + private boolean _sendError; protected HttpChannelState(HttpChannel channel) { @@ -252,11 +253,11 @@ protected Action handling() switch (_asyncRead) { case POSSIBLE: - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncRead = AsyncRead.PRODUCING; return Action.READ_PRODUCE; case READY: - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncRead = AsyncRead.IDLE; return Action.READ_CALLBACK; case REGISTER: @@ -270,7 +271,7 @@ protected Action handling() if (_asyncWritePossible) { - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncWritePossible = false; return Action.WRITE_CALLBACK; } @@ -302,7 +303,6 @@ protected Action handling() case ASYNC_ERROR: return Action.ASYNC_ERROR; - case ASYNC_IO: case ASYNC_WAIT: case DISPATCHED: case UPGRADED: @@ -361,45 +361,6 @@ public String toString() } } - public void asyncError(Throwable failure) - { - AsyncContextEvent event = null; - try (Locker.Lock lock = _locker.lock()) - { - switch (_state) - { - case IDLE: - case DISPATCHED: - case COMPLETING: - case COMPLETED: - case UPGRADED: - case ASYNC_IO: - case ASYNC_WOKEN: - case ASYNC_ERROR: - { - break; - } - case ASYNC_WAIT: - { - _event.addThrowable(failure); - _state = State.ASYNC_ERROR; - event = _event; - break; - } - default: - { - throw new IllegalStateException(getStatusStringLocked()); - } - } - } - - if (event != null) - { - cancelTimeout(event); - runInContext(event, _channel); - } - } - /** * Signal that the HttpConnection has finished handling the request. * For blocking connectors, this call may block if the request has @@ -421,14 +382,15 @@ protected Action unhandle() { case COMPLETING: case COMPLETED: + if (_sendError) + { + _sendError = false; + _state = State.DISPATCHED; + return Action.ERROR_DISPATCH; + } return Action.TERMINATED; - case ERRORED: - _state = State.DISPATCHED; - return Action.ERROR_DISPATCH; - case DISPATCHED: - case ASYNC_IO: case ASYNC_ERROR: case ASYNC_WAIT: break; @@ -438,11 +400,28 @@ protected Action unhandle() } _initial = false; + switch (_async) { - case COMPLETE: + case NOT_ASYNC: + if (_sendError) + { + _sendError = false; + _state = State.DISPATCHED; + return Action.ERROR_DISPATCH; + } _state = State.COMPLETING; + return Action.COMPLETE; + + case COMPLETE: _async = Async.NOT_ASYNC; + if (_sendError) + { + _sendError = false; + _state = State.DISPATCHED; + return Action.ERROR_DISPATCH; + } + _state = State.COMPLETING; return Action.COMPLETE; case DISPATCH: @@ -454,12 +433,12 @@ protected Action unhandle() switch (_asyncRead) { case READY: - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncRead = AsyncRead.IDLE; return Action.READ_CALLBACK; case POSSIBLE: - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncRead = AsyncRead.PRODUCING; return Action.READ_PRODUCE; @@ -476,7 +455,7 @@ protected Action unhandle() if (_asyncWritePossible) { - _state = State.ASYNC_IO; + _state = State.DISPATCHED; _asyncWritePossible = false; return Action.WRITE_CALLBACK; } @@ -504,10 +483,6 @@ protected Action unhandle() _async = Async.NOT_ASYNC; return Action.ERROR_DISPATCH; - case NOT_ASYNC: - _state = State.COMPLETING; - return Action.COMPLETE; - default: _state = State.COMPLETING; return Action.COMPLETE; @@ -555,7 +530,6 @@ public void dispatch(ServletContext context, String path) switch (_state) { case DISPATCHED: - case ASYNC_IO: case ASYNC_WOKEN: break; case ASYNC_WAIT: @@ -661,7 +635,7 @@ public String toString() { if (LOG.isDebugEnabled()) LOG.debug("Error after async timeout {}", this, th); - onError(th); + thrownError(th); } if (dispatch) @@ -681,30 +655,29 @@ public void complete() if (LOG.isDebugEnabled()) LOG.debug("complete {}", toStringLocked()); - boolean started = false; event = _event; - switch (_async) { case STARTED: - started = true; + _async = Async.COMPLETE; + if (_state == State.ASYNC_WAIT) + { + handle = true; + _state = State.ASYNC_WOKEN; + } break; + case EXPIRING: case ERRORING: - case ERRORED: + case ERRORED: // TODO ISE???? + _async = Async.COMPLETE; break; + case COMPLETE: return; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.COMPLETE; - - if (started && _state == State.ASYNC_WAIT) - { - handle = true; - _state = State.ASYNC_WOKEN; - } } cancelTimeout(event); @@ -727,7 +700,45 @@ public void errorComplete() cancelTimeout(); } - protected void onError(Throwable th) + public void asyncError(Throwable failure) + { + AsyncContextEvent event = null; + try (Locker.Lock lock = _locker.lock()) + { + switch (_state) + { + case IDLE: + case DISPATCHED: + case COMPLETING: + case COMPLETED: + case UPGRADED: + case ASYNC_WOKEN: + case ASYNC_ERROR: + { + break; + } + case ASYNC_WAIT: + { + _event.addThrowable(failure); + _state = State.ASYNC_ERROR; + event = _event; + break; + } + default: + { + throw new IllegalStateException(getStatusStringLocked()); + } + } + } + + if (event != null) + { + cancelTimeout(event); + runInContext(event, _channel); + } + } + + protected void thrownError(Throwable th) { int code = HttpStatus.INTERNAL_SERVER_ERROR_500; String message = null; @@ -747,18 +758,48 @@ else if (cause instanceof UnavailableException) code = HttpStatus.SERVICE_UNAVAILABLE_503; } - onError(th, code, message); + sendError(th, code, message); + + + } - public void onError(Throwable cause, int code, String message) + public void sendError(Throwable cause, int code, String message) { - final List listeners; - final AsyncContextEvent event; final Request baseRequest = _channel.getRequest(); if (message == null) message = cause == null ? HttpStatus.getMessage(code) : cause.toString(); + try (Locker.Lock lock = _locker.lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("onError {} {}", toStringLocked(), cause); + + switch (_state) + { + case DISPATCHED: + case COMPLETING: + case ASYNC_WOKEN: + case ASYNC_ERROR: + case ASYNC_WAIT: + _sendError = true; + if (_event != null) + // Add cause to async event to be handled by loser of race + // between unhandle and complete + _event.addThrowable(cause == null ? SEND_ERROR_CAUSE : cause); + break; + + case IDLE: + case COMPLETED: + case UPGRADED: + default: + { + throw new IllegalStateException(getStatusStringLocked()); + } + } + } + // we are allowed to have a body, then produce the error page. ContextHandler.Context context = baseRequest.getErrorContext(); if (context != null) @@ -769,25 +810,15 @@ public void onError(Throwable cause, int code, String message) baseRequest.setAttribute(ERROR_EXCEPTION, cause); baseRequest.setAttribute(ERROR_EXCEPTION_TYPE, cause == null ? null : cause.getClass()); baseRequest.setAttribute(ERROR_MESSAGE, message); + } + + private void callAsyncOnError() + { + final List listeners; + final AsyncContextEvent event; try (Locker.Lock lock = _locker.lock()) { - if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), cause); - - // Set error on request. - if (_event != null) - _event.addThrowable(cause); - - // Are we blocking? - if (_async == Async.NOT_ASYNC) - { - _state = State.ERRORED; - return; - } - - // We are Async - _async = Async.ERRORING; listeners = _asyncListeners; event = _event; } @@ -935,7 +966,6 @@ protected void recycle() switch (_state) { case DISPATCHED: - case ASYNC_IO: throw new IllegalStateException(getStatusStringLocked()); case UPGRADED: return; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 7aa1dd3ae229..ba7040c2bd31 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -283,7 +283,6 @@ else if (filled < 0) case COMPLETING: case COMPLETED: case IDLE: - case ERRORED: case ASYNC_ERROR: getEndPoint().shutdownOutput(); break; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 7f2c405f63a5..fcc70e7a6f14 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -416,7 +416,7 @@ public void sendError(int code, String message) throws IOException sendProcessing(); break; default: - _channel.getState().onError(null, code, message); + _channel.getState().sendError(null, code, message); break; } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 156dc5dc1c7c..9740a1bf9987 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; @@ -52,7 +53,6 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -137,7 +137,6 @@ public void destroy() throws Exception void testErrorOverridesStatus() throws Exception { String response = _connector.getResponse("GET /error-and-status/anything HTTP/1.0\r\n\r\n"); - System.err.println(response); assertThat(response, Matchers.containsString("HTTP/1.1 594 594")); assertThat(response, Matchers.containsString("ERROR_PAGE: /GlobalErrorPage")); assertThat(response, Matchers.containsString("ERROR_MESSAGE: custom get error")); @@ -323,7 +322,6 @@ public void testBadMessage() throws Exception } @Test - @Disabled public void testAsyncErrorPage0() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) @@ -340,9 +338,7 @@ public void testAsyncErrorPage0() throws Exception } } - // TODO re-enable once async is implemented @Test - @Disabled public void testAsyncErrorPage1() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) @@ -450,12 +446,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t { // Complete after original servlet hold.countDown(); - // Wait until request is recycled - while (Request.getBaseRequest(request).getMetaData() != null) + + // Wait until request async waiting + while (Request.getBaseRequest(request).getHttpChannelState().getState() != HttpChannelState.State.ASYNC_WAIT) { try { - Thread.sleep(100); + Thread.sleep(10); } catch (InterruptedException e) { From 0b0f19a7631bb8f62de5bb5823554aba06cd3cd8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 18 Jul 2019 10:11:42 +0200 Subject: [PATCH 17/57] Issue #3806 async sendError WIP making sendError simpler and more tests pass Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 5 +- .../jetty/server/HttpChannelState.java | 26 +++--- .../org/eclipse/jetty/server/Response.java | 4 - .../jetty/server/AbstractHttpTest.java | 14 ++-- .../server/HttpManyWaysToAsyncCommitTest.java | 68 ++++++++-------- .../eclipse/jetty/server/ResponseTest.java | 80 +++++++------------ 6 files changed, 88 insertions(+), 109 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 139dcbf9abce..a282a08f143e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -620,7 +620,10 @@ else if (noStack != null) LOG.warn(_request.getRequestURI(), failure); } - _state.thrownError(failure); + if (_response.isCommitted()) + abort(failure); + else + _state.thrownError(failure); } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index ab90ce3be54d..3a6b9985da17 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -759,14 +759,15 @@ else if (cause instanceof UnavailableException) } sendError(th, code, message); - - - } public void sendError(Throwable cause, int code, String message) { - final Request baseRequest = _channel.getRequest(); + final Request request = _channel.getRequest(); + final Response response = _channel.getResponse(); + + response.reset(true); + response.getHttpOutput().sendErrorClose(); if (message == null) message = cause == null ? HttpStatus.getMessage(code) : cause.toString(); @@ -800,16 +801,17 @@ public void sendError(Throwable cause, int code, String message) } } + request.getResponse().setStatus(code); // we are allowed to have a body, then produce the error page. - ContextHandler.Context context = baseRequest.getErrorContext(); + ContextHandler.Context context = request.getErrorContext(); if (context != null) - baseRequest.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - baseRequest.setAttribute(ERROR_REQUEST_URI, baseRequest.getRequestURI()); - baseRequest.setAttribute(ERROR_SERVLET_NAME, baseRequest.getServletName()); - baseRequest.setAttribute(ERROR_STATUS_CODE, code); - baseRequest.setAttribute(ERROR_EXCEPTION, cause); - baseRequest.setAttribute(ERROR_EXCEPTION_TYPE, cause == null ? null : cause.getClass()); - baseRequest.setAttribute(ERROR_MESSAGE, message); + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_EXCEPTION, cause); + request.setAttribute(ERROR_EXCEPTION_TYPE, cause == null ? null : cause.getClass()); + request.setAttribute(ERROR_MESSAGE, message); } private void callAsyncOnError() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index fcc70e7a6f14..8786ae8293cc 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -403,10 +403,6 @@ public void sendError(int code, String message) throws IOException if (isIncluding()) return; - reset(true); - - _out.sendErrorClose(); - switch (code) { case -1: diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java index f88d9c812608..d554043205c5 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java @@ -43,9 +43,7 @@ public abstract class AbstractHttpTest { - private static final Set __noBodyCodes = new HashSet<>(Arrays.asList(new String[]{ - "100", "101", "102", "204", "304" - })); + private static final Set __noBodyCodes = new HashSet<>(Arrays.asList("100", "101", "102", "204", "304")); protected static Server server; protected static ServerConnector connector; @@ -86,11 +84,11 @@ protected HttpTester.Response executeRequest(HttpVersion httpVersion) throws URI HttpTester.Input input = HttpTester.from(socket.getInputStream()); HttpTester.parseResponse(input, response); - if (httpVersion.is("HTTP/1.1") - && response.isComplete() - && response.get("content-length") == null - && response.get("transfer-encoding") == null - && !__noBodyCodes.contains(response.getStatus())) + if (httpVersion.is("HTTP/1.1") && + response.isComplete() && + response.get("content-length") == null && + response.get("transfer-encoding") == null && + !__noBodyCodes.contains(response.getStatus())) assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " + "it should contain connection:close", response.get("connection"), is("close")); return response; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index 7497a6b554a1..4acab838b986 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -44,7 +44,7 @@ //TODO: add protocol specific tests for connection: close and/or chunking public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { - private final String CONTEXT_ATTRIBUTE = getClass().getName() + ".asyncContext"; + private final String _contextAttribute = getClass().getName() + ".asyncContext"; public static Stream httpVersion() { @@ -101,10 +101,10 @@ private DoesNotSetHandledHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -115,7 +115,7 @@ public void run() else asyncContext.complete(); } - }).run(); + }).run(); // TODO this should be start for an async test! } super.doNonErrorHandle(target, baseRequest, request, response); } @@ -164,10 +164,10 @@ private OnlySetHandledHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -178,7 +178,7 @@ public void run() else asyncContext.complete(); } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -227,10 +227,10 @@ private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -249,7 +249,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -301,10 +301,10 @@ private ExplicitFlushHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -325,7 +325,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -377,10 +377,10 @@ private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -399,7 +399,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -453,10 +453,10 @@ private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -478,7 +478,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -533,10 +533,10 @@ private OverflowHandler(boolean throwException, boolean dispatch) @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -557,7 +557,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -610,10 +610,10 @@ private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -634,7 +634,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -688,10 +688,10 @@ private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -712,7 +712,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -761,10 +761,10 @@ private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -785,7 +785,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -835,10 +835,10 @@ private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean @Override public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) + if (request.getAttribute(_contextAttribute) == null) { final AsyncContext asyncContext = baseRequest.startAsync(); - request.setAttribute(CONTEXT_ATTRIBUTE, asyncContext); + request.setAttribute(_contextAttribute, asyncContext); new Thread(new Runnable() { @Override @@ -859,7 +859,7 @@ public void run() markFailed(e); } } - }).run(); + }).run(); // TODO this should be start for an async test! } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index a872ebdc8242..b4a811556f0b 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -35,6 +35,8 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.stream.Stream; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; @@ -70,6 +72,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; @@ -664,65 +668,41 @@ public void testContentTypeWithCharacterEncodingAndOther() throws Exception assertEquals("foo/bar; other=pq charset=utf-8 other=xyz;charset=utf-16", response.getContentType()); } - @Test - public void testStatusCodes() throws Exception + public static Stream sendErrorTestCodes() { - Response response = getResponse(); - - response.sendError(404); - assertEquals(404, response.getStatus()); - assertEquals("Not Found", response.getReason()); - - response = getResponse(); - - response.sendError(500, "Database Error"); - assertEquals(500, response.getStatus()); - assertEquals("Database Error", response.getReason()); - assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString())); - - response = getResponse(); - - response.setStatus(200); - assertEquals(200, response.getStatus()); - assertEquals(null, response.getReason()); - - response = getResponse(); - - response.sendError(406, "Super Nanny"); - assertEquals(406, response.getStatus()); - assertEquals("Super Nanny", response.getReason()); - assertEquals("must-revalidate,no-cache,no-store", response.getHeader(HttpHeader.CACHE_CONTROL.asString())); + List data = new ArrayList<>(); + data.add(new Object[]{404, null, "Not Found"}); + data.add(new Object[]{500, "Database Error", "Database Error"}); + data.add(new Object[]{406, "Super Nanny", "Super Nanny"}); + return data.stream(); } - @Test - public void testStatusCodesNoErrorHandler() throws Exception + @ParameterizedTest + @MethodSource(value = "sendErrorTestCodes") + public void testStatusCodes(int code, String message, String expectedMessage) throws Exception { - _server.removeBean(_server.getBean(ErrorHandler.class)); Response response = getResponse(); + assertThat(response.getHttpChannel().getState().handling(), is(HttpChannelState.Action.DISPATCH)); - response.sendError(404); - assertEquals(404, response.getStatus()); - assertEquals("Not Found", response.getReason()); - - response = getResponse(); - - response.sendError(500, "Database Error"); - assertEquals(500, response.getStatus()); - assertEquals("Database Error", response.getReason()); - assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue()); - - response = getResponse(); + if (message == null) + response.sendError(code); + else + response.sendError(code, message); - response.setStatus(200); - assertEquals(200, response.getStatus()); + assertTrue(response.getHttpOutput().isClosed()); + assertEquals(code, response.getStatus()); assertEquals(null, response.getReason()); + assertEquals(expectedMessage, response.getHttpChannel().getRequest().getAttribute(RequestDispatcher.ERROR_MESSAGE)); + assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.ERROR_DISPATCH)); + assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.COMPLETE)); + } - response = getResponse(); - - response.sendError(406, "Super Nanny"); - assertEquals(406, response.getStatus()); - assertEquals("Super Nanny", response.getReason()); - assertThat(response.getHeader(HttpHeader.CACHE_CONTROL.asString()), Matchers.nullValue()); + @ParameterizedTest + @MethodSource(value = "sendErrorTestCodes") + public void testStatusCodesNoErrorHandler(int code, String message, String expectedMessage) throws Exception + { + _server.removeBean(_server.getBean(ErrorHandler.class)); + testStatusCodes(code, message, expectedMessage); } @Test From 72474d8a671c3d5864429becfb714a0e65c010ed Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 18 Jul 2019 14:55:12 +0200 Subject: [PATCH 18/57] Issue #3806 async sendError WIP handling async and thrown exceptions Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 2 +- .../jetty/server/HttpChannelState.java | 232 +++++++++++------- .../org/eclipse/jetty/server/Response.java | 2 +- .../server/HttpManyWaysToAsyncCommitTest.java | 1 + 4 files changed, 143 insertions(+), 94 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index a282a08f143e..90d96a174e1b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -623,7 +623,7 @@ else if (noStack != null) if (_response.isCommitted()) abort(failure); else - _state.thrownError(failure); + _state.thrownException(failure); } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 3a6b9985da17..ecf7b59225ae 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -635,7 +635,7 @@ public String toString() { if (LOG.isDebugEnabled()) LOG.debug("Error after async timeout {}", this, th); - thrownError(th); + thrownException(th); } if (dispatch) @@ -702,6 +702,12 @@ public void errorComplete() public void asyncError(Throwable failure) { + // This method is called when an failure occurs asynchronously to + // normal handling. If the request is async, we arrange for the + // exception to be thrown from the normal handling loop and then + // actually handled by #thrownException + // TODO can we just directly call thrownException? + AsyncContextEvent event = null; try (Locker.Lock lock = _locker.lock()) { @@ -738,13 +744,19 @@ public void asyncError(Throwable failure) } } - protected void thrownError(Throwable th) + protected void thrownException(Throwable th) { + // This method is called by HttpChannel.handleException to handle an exception thrown from a dispatch: + // + If the request is async, then any async listeners are give a chance to handle the exception in their onError handler. + // + If the request is not async, or not handled by any async onError listener, then a normal sendError is done. + + // Determine the actual details of the exception int code = HttpStatus.INTERNAL_SERVER_ERROR_500; String message = null; Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - - if (cause instanceof BadMessageException) + if (cause == null) + cause = th; + else if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; code = bme.getCode(); @@ -758,85 +770,61 @@ else if (cause instanceof UnavailableException) code = HttpStatus.SERVICE_UNAVAILABLE_503; } - sendError(th, code, message); - } - - public void sendError(Throwable cause, int code, String message) - { - final Request request = _channel.getRequest(); - final Response response = _channel.getResponse(); - - response.reset(true); - response.getHttpOutput().sendErrorClose(); - - if (message == null) - message = cause == null ? HttpStatus.getMessage(code) : cause.toString(); - + // Check state to see if we are async or not + final AsyncContextEvent asyncEvent; + final List asyncListeners; + boolean dispatch = false; try (Locker.Lock lock = _locker.lock()) { - if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), cause); - - switch (_state) + switch (_async) { - case DISPATCHED: - case COMPLETING: - case ASYNC_WOKEN: - case ASYNC_ERROR: - case ASYNC_WAIT: - _sendError = true; - if (_event != null) - // Add cause to async event to be handled by loser of race - // between unhandle and complete - _event.addThrowable(cause == null ? SEND_ERROR_CAUSE : cause); + case NOT_ASYNC: + // error not in async, will be handled by error handler in normal handler loop. + asyncEvent = null; + asyncListeners = null; + break; + + case STARTED: + asyncEvent = _event; + if (_asyncListeners == null || _asyncListeners.isEmpty()) + { + // error in async, but no listeners so handle it with error dispatch + asyncListeners = null; + _async = Async.ERRORED; + if (_state == State.ASYNC_WAIT) + { + _state = State.ASYNC_WOKEN; + dispatch = true; + } + } + else + { + // error in async with listeners, so give them a chance to handle + asyncListeners = _asyncListeners; + _async = Async.ERRORING; + } break; - case IDLE: - case COMPLETED: - case UPGRADED: default: - { - throw new IllegalStateException(getStatusStringLocked()); - } + LOG.warn("unhandled in state " + _async, cause); + return; } } - request.getResponse().setStatus(code); - // we are allowed to have a body, then produce the error page. - ContextHandler.Context context = request.getErrorContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); - request.setAttribute(ERROR_STATUS_CODE, code); - request.setAttribute(ERROR_EXCEPTION, cause); - request.setAttribute(ERROR_EXCEPTION_TYPE, cause == null ? null : cause.getClass()); - request.setAttribute(ERROR_MESSAGE, message); - } - - private void callAsyncOnError() - { - final List listeners; - final AsyncContextEvent event; - - try (Locker.Lock lock = _locker.lock()) - { - listeners = _asyncListeners; - event = _event; - } - - if (listeners != null) + // If we are async and have async listeners + if (asyncEvent != null && asyncListeners != null) { + // call onError Runnable task = new Runnable() { @Override public void run() { - for (AsyncListener listener : listeners) + for (AsyncListener listener : asyncListeners) { try { - listener.onError(event); + listener.onError(asyncEvent); } catch (Throwable x) { @@ -852,40 +840,43 @@ public String toString() return "onError"; } }; - runInContext(event, task); - } + runInContext(asyncEvent, task); - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) + // check the actions of the listeners + try (Locker.Lock lock = _locker.lock()) { - case ERRORING: - { - // Still in this state ? The listeners did not invoke API methods - // and the container must provide a default error dispatch. - _async = Async.ERRORED; - break; - } - case DISPATCH: - case COMPLETE: + switch (_async) { - // The listeners called dispatch() or complete(). - break; - } - default: - { - throw new IllegalStateException(toString()); - } - } + case ERRORING: + // Still in ERROR state? The listeners did not invoke API methods + // and the container must provide a default error dispatch. + _async = Async.ERRORED; + if (_state == State.ASYNC_WAIT) + { + _state = State.ASYNC_WOKEN; + dispatch = true; + } + break; - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; + case DISPATCH: + case COMPLETE: + case NOT_ASYNC: + // The listeners handled the exception by calling dispatch() or complete(). + return; + + default: + LOG.warn("unhandled in state " + _async, cause); + return; + } } } + // handle the exception with a sendError dispatch + final Request request = _channel.getRequest(); + request.setAttribute(ERROR_EXCEPTION, cause); + request.setAttribute(ERROR_EXCEPTION_TYPE, cause.getClass()); + sendError(code, message); + if (dispatch) { if (LOG.isDebugEnabled()) @@ -894,6 +885,63 @@ public String toString() } } + public void sendError(int code, String message) + { + // This method is called by Response.sendError to organise for an error page to be generated when it is possible: + // + The response is reset and temporarily closed. + // + The details of the error are saved as request attributes + // + The _sendError boolean is set to true so that an ERROR_DISPATCH action will be generated: + // - after unhandle for sync + // - after both unhandle and complete for async + + final Request request = _channel.getRequest(); + final Response response = _channel.getResponse(); + + response.reset(true); + response.getHttpOutput().sendErrorClose(); + + if (message == null) + message = HttpStatus.getMessage(code); + + request.getResponse().setStatus(code); + // we are allowed to have a body, then produce the error page. + ContextHandler.Context context = request.getErrorContext(); + if (context != null) + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + + try (Locker.Lock lock = _locker.lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("sendError {}", toStringLocked()); + + switch (_state) + { + case DISPATCHED: + case COMPLETING: + case ASYNC_WOKEN: + case ASYNC_ERROR: + case ASYNC_WAIT: + _sendError = true; + if (_event != null) + _event.addThrowable(SEND_ERROR_CAUSE); + break; + + case IDLE: + case COMPLETED: + case UPGRADED: + default: + { + throw new IllegalStateException(getStatusStringLocked()); + } + } + } + + } + protected void completed() { final List aListeners; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 8786ae8293cc..1866988199d2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -412,7 +412,7 @@ public void sendError(int code, String message) throws IOException sendProcessing(); break; default: - _channel.getState().sendError(null, code, message); + _channel.getState().sendError(code, message); break; } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index 4acab838b986..83388a9364b6 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -42,6 +42,7 @@ //TODO: reset buffer tests //TODO: add protocol specific tests for connection: close and/or chunking +//TODO: make it really async by using start instead of run public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { private final String _contextAttribute = getClass().getName() + ".asyncContext"; From ab7f8e7d641971443102f03c5a9d911380052d9d Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 18 Jul 2019 17:23:31 +0200 Subject: [PATCH 19/57] Issue #3806 async sendError WIP passing tests Signed-off-by: Greg Wilkins --- .../jetty/server/HttpChannelState.java | 4 +- .../server/HttpManyWaysToAsyncCommitTest.java | 810 +++++++++++------- 2 files changed, 506 insertions(+), 308 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index ecf7b59225ae..a5f77cd0a4f4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -806,7 +806,7 @@ else if (cause instanceof UnavailableException) break; default: - LOG.warn("unhandled in state " + _async, cause); + LOG.warn("unhandled in state " + _async, new IllegalStateException(cause)); return; } } @@ -865,7 +865,7 @@ public String toString() return; default: - LOG.warn("unhandled in state " + _async, cause); + LOG.warn("unhandled in state " + _async, new IllegalStateException(cause)); return; } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index 83388a9364b6..c7a608f68013 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -21,6 +21,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Stream; import javax.servlet.AsyncContext; import javax.servlet.ServletException; @@ -31,18 +34,20 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.util.log.StacklessLogging; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeaderValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; //TODO: reset buffer tests //TODO: add protocol specific tests for connection: close and/or chunking -//TODO: make it really async by using start instead of run public class HttpManyWaysToAsyncCommitTest extends AbstractHttpTest { private final String _contextAttribute = getClass().getName() + ".asyncContext"; @@ -52,51 +57,89 @@ public static Stream httpVersion() // boolean dispatch - if true we dispatch, otherwise we complete final boolean DISPATCH = true; final boolean COMPLETE = false; + final boolean IN_WAIT = true; + final boolean WHILE_DISPATCHED = false; List ret = new ArrayList<>(); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, IN_WAIT)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE, WHILE_DISPATCHED)); + ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE, WHILE_DISPATCHED)); return ret.stream(); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerDoesNotSetHandled(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerDoesNotSetHandled(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(false, dispatch); + DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(404)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(404)); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(true, dispatch); + DoesNotSetHandledHandler handler = new DoesNotSetHandledHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + int expected; + if (inWait) + { + // throw happens before async processing, so is handled + expected = 500; + } + else if (dispatch) + { + // throw happen again in async dispatch + expected = 500; + } + else + { + // complete happens before throw, so the throw is ignored and 404 is generated. + expected = 404; + } + + assertThat(response.getStatus(), is(expected)); + assertThat(handler.failure(), is(nullValue())); } private class DoesNotSetHandledHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private DoesNotSetHandledHandler(boolean throwException, boolean dispatch) + private DoesNotSetHandledHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -106,17 +149,13 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() - { - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - }).run(); // TODO this should be start for an async test! + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + }); } super.doNonErrorHandle(target, baseRequest, request, response); } @@ -124,42 +163,60 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledTrueOnly(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OnlySetHandledHandler handler = new OnlySetHandledHandler(false, dispatch); + OnlySetHandledHandler handler = new OnlySetHandledHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "0")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OnlySetHandledHandler handler = new OnlySetHandledHandler(true, dispatch); + OnlySetHandledHandler handler = new OnlySetHandledHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } + + // If async happens during dispatch it can generate 200 before exception + int expected = inWait ? 500 : (dispatch ? 500 : 200); - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(expected)); + assertThat(handler.failure(), is(nullValue())); } private class OnlySetHandledHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private OnlySetHandledHandler(boolean throwException, boolean dispatch) + private OnlySetHandledHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -169,17 +226,13 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() - { - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - }).run(); // TODO this should be start for an async test! + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -188,41 +241,71 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(false, dispatch); + SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "6")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(true, dispatch); + SetHandledWriteSomeDataHandler handler = new SetHandledWriteSomeDataHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); + HttpTester.Response response; + if (inWait) + { + // exception thrown and handled before any async processing + response = executeRequest(httpVersion); + } + else + { + // exception thrown after async processing, so cannot be handled + try (StacklessLogging log = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } + } - HttpTester.Response response = executeRequest(httpVersion); + if (inWait) + { + // Throw is done before async action and can be handled + assertThat(response.getStatus(), is(500)); + } + else if (dispatch) + { + // async dispatch is thrown again + assertThat(response.getStatus(), is(500)); + } + else + { + // async is done before exception, so exception is not handled + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foobar")); + } - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch) + private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -232,25 +315,21 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - asyncContext.getResponse().getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + asyncContext.getResponse().getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); // TODO this should be start for an async test! + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -259,44 +338,55 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerExplicitFlush(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerExplicitFlush(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - ExplicitFlushHandler handler = new ExplicitFlushHandler(false, dispatch); + ExplicitFlushHandler handler = new ExplicitFlushHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandlerExplicitFlushAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - ExplicitFlushHandler handler = new ExplicitFlushHandler(true, dispatch); + ExplicitFlushHandler handler = new ExplicitFlushHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // throw happens before flush + assertThat(response.getStatus(), is(500)); + } + else + { + // flush happens before throw + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + assertThat(handler.failure(), is(nullValue())); } private class ExplicitFlushHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private ExplicitFlushHandler(boolean throwException, boolean dispatch) + private ExplicitFlushHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -306,27 +396,23 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foobar"); - asyncContextResponse.flushBuffer(); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foobar"); + asyncContextResponse.flushBuffer(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + catch (IOException e) + { + markFailed(e); } - }).run(); // TODO this should be start for an async test! + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -335,44 +421,55 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testHandledAndFlushWithoutContent(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandledAndFlushWithoutContent(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(false, dispatch); + SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); } @ParameterizedTest @MethodSource("httpVersion") - public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testHandledAndFlushWithoutContentAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(true, dispatch); + SetHandledAndFlushWithoutContentHandler handler = new SetHandledAndFlushWithoutContentHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // throw happens before async behaviour, so is handled + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + + assertThat(handler.failure(), is(nullValue())); } private class SetHandledAndFlushWithoutContentHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean dispatch) + private SetHandledAndFlushWithoutContentHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -382,25 +479,21 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try + { + asyncContext.getResponse().flushBuffer(); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + catch (IOException e) { - try - { - asyncContext.getResponse().flushBuffer(); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + markFailed(e); } - }).run(); // TODO this should be start for an async test! + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -409,16 +502,16 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(false, dispatch); + WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); // HTTP/1.0 does not do chunked. it will just send content and close if (httpVersion.is("HTTP/1.1")) @@ -427,28 +520,39 @@ public void testWriteFlushWriteMore(HttpVersion httpVersion, boolean dispatch) t @ParameterizedTest @MethodSource("httpVersion") - public void testWriteFlushWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteFlushWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(true, dispatch); + WriteFlushWriteMoreHandler handler = new WriteFlushWriteMoreHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + if (inWait) + { + // The exception is thrown before we do any writing or async operations, so it delivered as onError and then + // dispatched. + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + } + assertThat(handler.failure(), is(nullValue())); } private class WriteFlushWriteMoreHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch) + private WriteFlushWriteMoreHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -458,28 +562,24 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try + { + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foo"); + asyncContextResponse.flushBuffer(); + asyncContextResponse.getWriter().write("bar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + catch (IOException e) { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foo"); - asyncContextResponse.flushBuffer(); - asyncContextResponse.getWriter().write("bar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + markFailed(e); } - }).run(); // TODO this should be start for an async test! + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -488,47 +588,58 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testBufferOverflow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testBufferOverflow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OverflowHandler handler = new OverflowHandler(false, dispatch); + OverflowHandler handler = new OverflowHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); assertThat(response.getContent(), is("foobar")); if (httpVersion.is("HTTP/1.1")) assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testBufferOverflowAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testBufferOverflowAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - OverflowHandler handler = new OverflowHandler(true, dispatch); + OverflowHandler handler = new OverflowHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // Buffer size is too small, so the content is written directly producing a 200 response - assertThat("response code", response.getStatus(), is(200)); - assertThat(response.getContent(), is("foobar")); - if (httpVersion.is("HTTP/1.1")) - assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + // Buffer size smaller than content, so writing will commit response. + // If this happens before the exception is thrown we get a 200, else a 500 is produced + if (inWait) + { + assertThat(response.getStatus(), is(500)); + assertThat(response.getContent(), containsString("TestCommitException: Thrown by test")); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foobar")); + if (httpVersion.is("HTTP/1.1")) + assertThat(response, containsHeaderValue(HttpHeader.TRANSFER_ENCODING, "chunked")); + assertThat(handler.failure(), is(nullValue())); + } } private class OverflowHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private OverflowHandler(boolean throwException, boolean dispatch) + private OverflowHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -538,27 +649,23 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setBufferSize(3); - asyncContextResponse.getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setBufferSize(3); + asyncContextResponse.getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); // TODO this should be start for an async test! + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -567,45 +674,54 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteExactlyThatAmountOfBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(false, dispatch); + SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteExactlyThatAmountOfBytesAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(true, dispatch); + SetContentLengthAndWriteThatAmountOfBytesHandler handler = new SetContentLengthAndWriteThatAmountOfBytesHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - //TODO: should we expect 500 here? - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late! + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); + } + assertThat(handler.failure(), is(nullValue())); } private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, boolean dispatch) + private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -615,27 +731,23 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setContentLength(3); - asyncContextResponse.getWriter().write("foo"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setContentLength(3); + asyncContextResponse.getWriter().write("foo"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); // TODO this should be start for an async test! + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -644,46 +756,55 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteMoreBytes(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(false, dispatch); + SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); + assertThat(response.getStatus(), is(200)); // jetty truncates the body when content-length is reached.! This is correct and desired behaviour? - assertThat("response body", response.getContent(), is("foo")); + assertThat(response.getContent(), is("foo")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } @ParameterizedTest @MethodSource("httpVersion") - public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(true, dispatch); + SetContentLengthAndWriteMoreBytesHandler handler = new SetContentLengthAndWriteMoreBytesHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // TODO: we throw before response is committed. should we expect 500? - assertThat("response code", response.getStatus(), is(200)); - assertThat("response body", response.getContent(), is("foo")); + if (inWait) + { + // too late! + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(response.getContent(), is("foo")); + } assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(handler.failure(), is(nullValue())); } private class SetContentLengthAndWriteMoreBytesHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean dispatch) + private SetContentLengthAndWriteMoreBytesHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -693,27 +814,23 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try + { + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.setContentLength(3); + asyncContextResponse.getWriter().write("foobar"); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + catch (IOException e) { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.setContentLength(3); - asyncContextResponse.getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + markFailed(e); } - }).run(); // TODO this should be start for an async test! + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -722,41 +839,50 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLength(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLength(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(false, dispatch); + WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), is(nullValue())); //TODO: jetty ignores setContentLength and sends transfer-encoding header. Correct? } @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(true, dispatch); + WriteAndSetContentLengthHandler handler = new WriteAndSetContentLengthHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - - assertThat("response code", response.getStatus(), is(200)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late + assertThat(response.getStatus(), is(500)); + } + else + { + assertThat(response.getStatus(), is(200)); + } + assertThat(handler.failure(), is(nullValue())); } private class WriteAndSetContentLengthHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch) + private WriteAndSetContentLengthHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -766,27 +892,23 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foo"); - asyncContextResponse.setContentLength(3); // This should commit the response - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foo"); + asyncContextResponse.setContentLength(3); // This should commit the response + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); } - }).run(); // TODO this should be start for an async test! + catch (IOException e) + { + markFailed(e); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); @@ -795,42 +917,64 @@ public void run() @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthTooSmall(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(false, dispatch); + WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(false, dispatch, inWait); server.setHandler(handler); server.start(); HttpTester.Response response = executeRequest(httpVersion); - // Setting a content-length too small throws an IllegalStateException - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + // Setting a content-length too small throws an IllegalStateException, + // but only in the async handler, which completes or dispatches anyway + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), not(is(nullValue()))); } @ParameterizedTest @MethodSource("httpVersion") - public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion, boolean dispatch) throws Exception + public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion, boolean dispatch, boolean inWait) throws Exception { - WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(true, dispatch); + WriteAndSetContentLengthTooSmallHandler handler = new WriteAndSetContentLengthTooSmallHandler(true, dispatch, inWait); server.setHandler(handler); server.start(); - HttpTester.Response response = executeRequest(httpVersion); + HttpTester.Response response; + try (StacklessLogging stackless = new StacklessLogging(HttpChannelState.class)) + { + response = executeRequest(httpVersion); + } // Setting a content-length too small throws an IllegalStateException - assertThat("response code", response.getStatus(), is(500)); - assertThat("no exceptions", handler.failure(), is(nullValue())); + if (inWait) + { + // too late + assertThat(response.getStatus(), is(500)); + assertThat(handler.failure(), is(nullValue())); + } + else if (dispatch) + { + // throw on async dispatch + assertThat(response.getStatus(), is(500)); + assertThat(handler.failure(), not(is(nullValue()))); + } + else + { + assertThat(response.getStatus(), is(200)); + assertThat(handler.failure(), not(is(nullValue()))); + } } private class WriteAndSetContentLengthTooSmallHandler extends ThrowExceptionOnDemandHandler { private final boolean dispatch; + private final boolean inWait; - private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean dispatch) + private WriteAndSetContentLengthTooSmallHandler(boolean throwException, boolean dispatch, boolean inWait) { super(throwException); this.dispatch = dispatch; + this.inWait = inWait; } @Override @@ -840,30 +984,84 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl { final AsyncContext asyncContext = baseRequest.startAsync(); request.setAttribute(_contextAttribute, asyncContext); - new Thread(new Runnable() + runAsync(baseRequest, inWait, () -> { - @Override - public void run() + try { - try - { - ServletResponse asyncContextResponse = asyncContext.getResponse(); - asyncContextResponse.getWriter().write("foobar"); - asyncContextResponse.setContentLength(3); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - } - catch (IOException e) - { - markFailed(e); - } + ServletResponse asyncContextResponse = asyncContext.getResponse(); + asyncContextResponse.getWriter().write("foobar"); + asyncContextResponse.setContentLength(3); } - }).run(); // TODO this should be start for an async test! + catch (Throwable e) + { + markFailed(e); + if (dispatch) + asyncContext.dispatch(); + else + asyncContext.complete(); + } + }); } baseRequest.setHandled(true); super.doNonErrorHandle(target, baseRequest, request, response); } } + + private void runAsyncInAsyncWait(Request request, Runnable task) + { + new Thread(() -> + { + long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + + try + { + while (System.nanoTime() < end && request.getHttpChannelState().getState() != HttpChannelState.State.ASYNC_WAIT) + { + Thread.sleep(100); + } + if (request.getHttpChannelState().getState() == HttpChannelState.State.ASYNC_WAIT) + task.run(); + else + request.getHttpChannel().abort(new TimeoutException()); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + }).start(); + } + + private void runAsyncWhileDispatched(Runnable task) + { + CountDownLatch ran = new CountDownLatch(1); + + new Thread(() -> + { + try + { + task.run(); + } + finally + { + ran.countDown(); + } + }).start(); + + try + { + ran.await(10, TimeUnit.SECONDS); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + + private void runAsync(Request request, boolean inWait, Runnable task) + { + if (inWait) + runAsyncInAsyncWait(request, task); + else + runAsyncWhileDispatched(task); + } } From 8f1ec06059d94d62454a95af4285d513371c854f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 19 Jul 2019 11:37:07 +0200 Subject: [PATCH 20/57] Issue #3806 async sendError Improved thread handling Signed-off-by: Greg Wilkins --- .../server/HttpManyWaysToAsyncCommitTest.java | 31 ++++++++++++------- .../jetty/util/thread/QueuedThreadPool.java | 8 +++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index c7a608f68013..61493932d799 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -1009,33 +1009,42 @@ public void doNonErrorHandle(String target, Request baseRequest, final HttpServl private void runAsyncInAsyncWait(Request request, Runnable task) { - new Thread(() -> + server.getThreadPool().execute(() -> { long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); - try { - while (System.nanoTime() < end && request.getHttpChannelState().getState() != HttpChannelState.State.ASYNC_WAIT) + while (System.nanoTime() < end) { - Thread.sleep(100); + switch (request.getHttpChannelState().getState()) + { + case ASYNC_WAIT: + task.run(); + return; + + case DISPATCHED: + Thread.sleep(100); + continue; + + default: + request.getHttpChannel().abort(new IllegalStateException()); + return; + } } - if (request.getHttpChannelState().getState() == HttpChannelState.State.ASYNC_WAIT) - task.run(); - else - request.getHttpChannel().abort(new TimeoutException()); + request.getHttpChannel().abort(new TimeoutException()); } catch (InterruptedException e) { e.printStackTrace(); } - }).start(); + }); } private void runAsyncWhileDispatched(Runnable task) { CountDownLatch ran = new CountDownLatch(1); - new Thread(() -> + server.getThreadPool().execute(() -> { try { @@ -1045,7 +1054,7 @@ private void runAsyncWhileDispatched(Runnable task) { ran.countDown(); } - }).start(); + }); try { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index c2d77760c088..c987f4308282 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -184,11 +184,15 @@ protected void doStop() throws Exception BlockingQueue jobs = getQueue(); if (timeout > 0) { + // Consume any reserved threads + while (tryExecute(NOOP)) + { + ; + } + // Fill the job queue with noop jobs to wakeup idle threads. for (int i = 0; i < threads; ++i) - { jobs.offer(NOOP); - } // try to let jobs complete naturally for half our stop time joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2); From fb6680f04b3c1462d710e35898af7d63cc0276e3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 19 Jul 2019 11:38:47 +0200 Subject: [PATCH 21/57] Issue #3806 async sendError removed bad test Signed-off-by: Greg Wilkins --- ...ManyWaysToAsyncCommitBadBehaviourTest.java | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java deleted file mode 100644 index 500c292e0fb4..000000000000 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitBadBehaviourTest.java +++ /dev/null @@ -1,133 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.server; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; -import javax.servlet.AsyncContext; -import javax.servlet.DispatcherType; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.http.HttpVersion; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -//TODO: reset buffer tests -//TODO: add protocol specific tests for connection: close and/or chunking - -public class HttpManyWaysToAsyncCommitBadBehaviourTest extends AbstractHttpTest -{ - private final String CONTEXT_ATTRIBUTE = getClass().getName() + ".asyncContext"; - - public static Stream httpVersions() - { - // boolean dispatch - if true we dispatch, otherwise we complete - final boolean DISPATCH = true; - final boolean COMPLETE = false; - - List ret = new ArrayList<>(); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_0, COMPLETE)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, DISPATCH)); - ret.add(Arguments.of(HttpVersion.HTTP_1_1, COMPLETE)); - return ret.stream(); - } - - @ParameterizedTest - @MethodSource("httpVersions") - public void testHandlerSetsHandledAndWritesSomeContent(HttpVersion httpVersion, boolean dispatch) throws Exception - { - server.setHandler(new SetHandledWriteSomeDataHandler(false, dispatch)); - server.start(); - - HttpTester.Response response = executeRequest(httpVersion); - - assertThat("response code is 500", response.getStatus(), is(500)); - } - - private class SetHandledWriteSomeDataHandler extends ThrowExceptionOnDemandHandler - { - private final boolean dispatch; - - private SetHandledWriteSomeDataHandler(boolean throwException, boolean dispatch) - { - super(throwException); - this.dispatch = dispatch; - } - - @Override - public void doNonErrorHandle(String target, Request baseRequest, final HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - final CyclicBarrier resumeBarrier = new CyclicBarrier(1); - - if (baseRequest.getDispatcherType() == DispatcherType.ERROR) - { - response.sendError(500); - return; - } - - if (request.getAttribute(CONTEXT_ATTRIBUTE) == null) - { - final AsyncContext asyncContext = baseRequest.startAsync(); - new Thread(new Runnable() - { - @Override - public void run() - { - try - { - asyncContext.getResponse().getWriter().write("foobar"); - if (dispatch) - asyncContext.dispatch(); - else - asyncContext.complete(); - resumeBarrier.await(5, TimeUnit.SECONDS); - } - catch (IOException | TimeoutException | InterruptedException | BrokenBarrierException e) - { - e.printStackTrace(); - } - } - }).run(); - } - try - { - resumeBarrier.await(5, TimeUnit.SECONDS); - } - catch (InterruptedException | BrokenBarrierException | TimeoutException e) - { - e.printStackTrace(); - } - throw new TestCommitException(); - } - } -} From f02ce731007e0fc790d752f9fa9761f3ed522b88 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 19 Jul 2019 16:48:15 +0200 Subject: [PATCH 22/57] Issue #3806 async sendError Implemented error dispatch on complete properly more fixed tests Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 2 +- .../jetty/server/HttpChannelState.java | 11 +- .../java/org/eclipse/jetty/server/Server.java | 12 +- .../jetty/server/HttpServerTestBase.java | 107 ++++++++++-------- .../server/handler/NcsaRequestLogTest.java | 18 +-- .../handler/SecuredRedirectHandlerTest.java | 1 + .../ssl/SniSslConnectionFactoryTest.java | 63 ++++------- .../eclipse/jetty/servlet/ErrorPageTest.java | 1 - 8 files changed, 114 insertions(+), 101 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 90d96a174e1b..42777f00b5a4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -524,7 +524,7 @@ public boolean handle() case COMPLETE: { - if (!_response.isCommitted() && !_request.isHandled()) + if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed()) { _response.sendError(HttpStatus.NOT_FOUND_404); break; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index a5f77cd0a4f4..eb919c64fcfe 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -250,6 +250,14 @@ protected Action handling() return Action.TERMINATED; case ASYNC_WOKEN: + if (_sendError) + { + _sendError = false; + _async = Async.NOT_ASYNC; + _state = State.DISPATCHED; + return Action.ERROR_DISPATCH; + } + switch (_asyncRead) { case POSSIBLE: @@ -659,7 +667,7 @@ public void complete() switch (_async) { case STARTED: - _async = Async.COMPLETE; + _async = _sendError ? Async.NOT_ASYNC : Async.COMPLETE; if (_state == State.ASYNC_WAIT) { handle = true; @@ -669,7 +677,6 @@ public void complete() case EXPIRING: case ERRORING: - case ERRORED: // TODO ISE???? _async = Async.COMPLETE; break; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index da2b77e2a722..217df4f9cf3b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -485,10 +485,16 @@ public void handle(HttpChannel channel) throws IOException, ServletException if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target)) { if (!HttpMethod.OPTIONS.is(request.getMethod())) + { + request.setHandled(true); response.sendError(HttpStatus.BAD_REQUEST_400); - handleOptions(request, response); - if (!request.isHandled()) - handle(target, request, request, response); + } + else + { + handleOptions(request, response); + if (!request.isHandled()) + handle(target, request, request, response); + } } else handle(target, request, request, response); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index 5e804e79c4e0..72fd5740170a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -64,13 +64,22 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture { - private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" + "Host: localhost\n" + "Content-Type: text/xml; charset=utf-8\n" + "Connection: close\n" + "Content-Length: "; - private static final String REQUEST1_CONTENT = "\n" - + "\n" - + ""; + private static final String REQUEST1_HEADER = "POST / HTTP/1.0\n" + + "Host: localhost\n" + + "Content-Type: text/xml; charset=utf-8\n" + + "Connection: close\n" + + "Content-Length: "; + private static final String REQUEST1_CONTENT = "\n" + + "\n" + + ""; private static final String REQUEST1 = REQUEST1_HEADER + REQUEST1_CONTENT.getBytes().length + "\n\n" + REQUEST1_CONTENT; - private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + "Content-Length: 13\n" + "Server: Jetty(" + Server.getVersion() + ")\n" + "\n" + "Hello world\n"; + private static final String RESPONSE1 = "HTTP/1.1 200 OK\n" + + "Content-Length: 13\n" + + "Server: Jetty(" + Server.getVersion() + ")\n" + + "\n" + + "Hello world\n"; // Break the request up into three pieces, splitting the header. private static final String FRAGMENT1 = REQUEST1.substring(0, 16); @@ -102,8 +111,8 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture " \n" + " 73\n" + " \n" + - " \n" - + "\n"; + " \n" + + "\n"; protected static final String RESPONSE2 = "HTTP/1.1 200 OK\n" + "Content-Type: text/xml;charset=iso-8859-1\n" + @@ -141,10 +150,10 @@ public void testOPTIONS() throws Exception { OutputStream os = client.getOutputStream(); - os.write(("OPTIONS * HTTP/1.1\r\n" - + "Host: " + _serverURI.getHost() + "\r\n" - + "Connection: close\r\n" - + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.write(("OPTIONS * HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. @@ -153,15 +162,20 @@ public void testOPTIONS() throws Exception assertThat(response, Matchers.containsString("HTTP/1.1 200 OK")); assertThat(response, Matchers.containsString("Allow: GET")); } + } + @Test + public void testGETStar() throws Exception + { + configureServer(new OptionsHandler()); try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) { OutputStream os = client.getOutputStream(); - os.write(("GET * HTTP/1.1\r\n" - + "Host: " + _serverURI.getHost() + "\r\n" - + "Connection: close\r\n" - + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.write(("GET * HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); os.flush(); // Read the response. @@ -434,21 +448,21 @@ public void testFragmentedChunk() throws Exception { OutputStream os = client.getOutputStream(); - os.write(("GET /R2 HTTP/1.1\015\012" + - "Host: localhost\015\012" + - "Transfer-Encoding: chunked\015\012" + - "Content-Type: text/plain\015\012" + - "Connection: close\015\012" + - "\015\012").getBytes()); + os.write(("GET /R2 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes()); os.flush(); Thread.sleep(1000); os.write(("5").getBytes()); Thread.sleep(1000); - os.write(("\015\012").getBytes()); + os.write(("\r\n").getBytes()); os.flush(); Thread.sleep(1000); - os.write(("ABCDE\015\012" + - "0;\015\012\015\012").getBytes()); + os.write(("ABCDE\r\n" + + "0;\r\n\r\n").getBytes()); os.flush(); // Read the response. @@ -466,14 +480,14 @@ public void testTrailingContent() throws Exception { OutputStream os = client.getOutputStream(); - os.write(("GET /R2 HTTP/1.1\015\012" + - "Host: localhost\015\012" + - "Content-Length: 5\015\012" + - "Content-Type: text/plain\015\012" + - "Connection: close\015\012" + - "\015\012" + - "ABCDE\015\012" + - "\015\012" + os.write(("GET /R2 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: 5\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n" + + "ABCDE\r\n" + + "\r\n" ).getBytes()); os.flush(); @@ -1128,26 +1142,26 @@ public void testHead() throws Exception InputStream is = client.getInputStream(); os.write(( - "POST /R1 HTTP/1.1\015\012" + + "POST /R1 HTTP/1.1\r\n" + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "\015\012" + + "\r\n" + "123456789\n" + - "HEAD /R2 HTTP/1.1\015\012" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" + + "HEAD /R2 HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "\015\012" + + "\r\n" + "ABCDEFGHI\n" + - "POST /R3 HTTP/1.1\015\012" + - "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" + + "POST /R3 HTTP/1.1\r\n" + + "Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" + "content-type: text/plain; charset=utf-8\r\n" + "content-length: 10\r\n" + - "Connection: close\015\012" + - "\015\012" + + "Connection: close\r\n" + + "\r\n" + "abcdefghi\n" ).getBytes(StandardCharsets.ISO_8859_1)); @@ -1545,12 +1559,11 @@ public void run() { try { - byte[] bytes = ( - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" - + "Content-Length: " + cl + "\r\n" + - "\r\n" + - content).getBytes(StandardCharsets.ISO_8859_1); + byte[] bytes = ("GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: " + cl + "\r\n" + + "\r\n" + + content).getBytes(StandardCharsets.ISO_8859_1); for (int i = 0; i < REQS; i++) { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java index 4f119ff97fda..763d19eb5b1e 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java @@ -44,6 +44,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StacklessLogging; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -569,7 +570,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { request.setAttribute("ASYNC", Boolean.TRUE); AsyncContext ac = request.startAsync(); - ac.setTimeout(1000); + ac.setTimeout(100000); baseRequest.setHandled(true); _server.getThreadPool().execute(() -> { @@ -584,18 +585,21 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques } catch (BadMessageException bad) { - response.sendError(bad.getCode()); + response.sendError(bad.getCode(), bad.getReason()); } catch (Exception e) { - response.sendError(500); + response.sendError(500, e.toString()); } } - catch (Throwable th) + catch (IOException | IllegalStateException th) + { + Log.getLog().ignore(th); + } + finally { - throw new RuntimeException(th); + ac.complete(); } - ac.complete(); }); } } @@ -625,7 +629,7 @@ public void log(Request request, Response response) } } - private static abstract class AbstractTestHandler extends AbstractHandler + private abstract static class AbstractTestHandler extends AbstractHandler { @Override public String toString() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java index 4dd9dfbfa1a2..73d5193f2623 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java @@ -266,6 +266,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { if (!"/".equals(target)) { + baseRequest.setHandled(true); response.sendError(404); return; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index 688dc8bcfa4f..a24eda4337b4 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; @@ -42,6 +41,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; @@ -55,8 +55,8 @@ import org.eclipse.jetty.server.SocketCustomizationListener; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -65,15 +65,14 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; public class SniSslConnectionFactoryTest { private Server _server; private ServerConnector _connector; - private HttpConfiguration _https_config; + private HttpConfiguration _httpsConfig; private int _port; @BeforeEach @@ -85,11 +84,11 @@ public void before() throws Exception http_config.setSecureScheme("https"); http_config.setSecurePort(8443); http_config.setOutputBufferSize(32768); - _https_config = new HttpConfiguration(http_config); + _httpsConfig = new HttpConfiguration(http_config); SecureRequestCustomizer src = new SecureRequestCustomizer(); src.setSniHostCheck(true); - _https_config.addCustomizer(src); - _https_config.addCustomizer((connector, httpConfig, request) -> + _httpsConfig.addCustomizer(src); + _httpsConfig.addCustomizer((connector, httpConfig, request) -> { EndPoint endp = request.getHttpChannel().getEndPoint(); if (endp instanceof SslConnection.DecryptedEndPoint) @@ -126,7 +125,7 @@ protected void start(String keystorePath) throws Exception ServerConnector https = _connector = new ServerConnector(_server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), - new HttpConnectionFactory(_https_config)); + new HttpConnectionFactory(_httpsConfig)); _server.addConnector(https); _server.setHandler(new AbstractHandler.ErrorDispatchHandler() @@ -224,6 +223,7 @@ public void testBadSNIConnect() throws Exception public void testSameConnectionRequestsForManyDomains() throws Exception { start("src/test/resources/keystore_sni.p12"); + _server.setErrorHandler(new ErrorHandler()); SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); @@ -246,8 +246,8 @@ public void testSameConnectionRequestsForManyDomains() throws Exception output.flush(); InputStream input = sslSocket.getInputStream(); - String response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + HttpTester.Response response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Same socket, send a request for a different domain but same alias. request = @@ -256,9 +256,8 @@ public void testSameConnectionRequestsForManyDomains() throws Exception "\r\n"; output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Same socket, send a request for a different domain but different alias. request = @@ -268,9 +267,9 @@ public void testSameConnectionRequestsForManyDomains() throws Exception output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertThat(response, startsWith("HTTP/1.1 400 ")); - assertThat(response, containsString("Host does not match SNI")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(400)); + assertThat(response.getContent(), containsString("Host does not match SNI")); } finally { @@ -303,8 +302,8 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception output.flush(); InputStream input = sslSocket.getInputStream(); - String response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + HttpTester.Response response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Now, on the same socket, send a request for a different valid domain. request = @@ -314,8 +313,8 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 200 ")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(200)); // Now make a request for an invalid domain for this connection. request = @@ -325,9 +324,9 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception output.write(request.getBytes(StandardCharsets.UTF_8)); output.flush(); - response = response(input); - assertTrue(response.startsWith("HTTP/1.1 400 ")); - assertThat(response, Matchers.containsString("Host does not match SNI")); + response = HttpTester.parseResponse(input); + assertThat(response.getStatus(), is(400)); + assertThat(response.getContent(), containsString("Host does not match SNI")); } finally { @@ -335,22 +334,6 @@ public void testSameConnectionRequestsForManyWildDomains() throws Exception } } - private String response(InputStream input) throws IOException - { - Utf8StringBuilder buffer = new Utf8StringBuilder(); - int crlfs = 0; - while (true) - { - int read = input.read(); - assertTrue(read >= 0); - buffer.append((byte)read); - crlfs = (read == '\r' || read == '\n') ? crlfs + 1 : 0; - if (crlfs == 4) - break; - } - return buffer.toString(); - } - private String getResponse(String host, String cn) throws Exception { String response = getResponse(host, host, cn); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 9740a1bf9987..e733877da95c 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -224,7 +224,6 @@ void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception void testNestedSendErrorDoesNotLoop() throws Exception { String response = _connector.getResponse("GET /fail/code?code=597 HTTP/1.0\r\n\r\n"); - System.out.println(response); assertThat(response, Matchers.containsString("HTTP/1.1 597 597")); assertThat(response, not(Matchers.containsString("time this error page is being accessed"))); } From 3d3a4ab984ed526f48cb9b1a5a7e4cdce218824e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 20 Jul 2019 10:05:49 +0200 Subject: [PATCH 23/57] Issue #3806 async sendError sendError state looks committed Signed-off-by: Greg Wilkins --- .../main/java/org/eclipse/jetty/server/HttpChannel.java | 5 +++++ .../java/org/eclipse/jetty/server/HttpChannelState.java | 8 ++++++++ .../src/main/java/org/eclipse/jetty/server/Response.java | 2 +- .../jetty/server/HttpManyWaysToAsyncCommitTest.java | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 42777f00b5a4..96113a6003b2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -121,6 +121,11 @@ public HttpChannel(Connector connector, HttpConfiguration configuration, EndPoin _state); } + public boolean isSendError() + { + return _state.isSendError(); + } + protected HttpInput newHttpInput(HttpChannelState state) { return new HttpInput(state); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index eb919c64fcfe..97116805fb9a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -170,6 +170,14 @@ public boolean hasListener(AsyncListener listener) } } + public boolean isSendError() + { + try (Locker.Lock lock = _locker.lock()) + { + return _sendError; + } + } + public void setTimeout(long ms) { try (Locker.Lock lock = _locker.lock()) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 1866988199d2..f1bc7c6236d8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -1118,7 +1118,7 @@ public MetaData.Response getCommittedMetaData() @Override public boolean isCommitted() { - return _channel.isCommitted(); + return _channel.isCommitted() || _channel.isSendError(); } @Override diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index 61493932d799..c756ceeef61c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -790,8 +790,8 @@ public void testSetContentLengthAndWriteMoreAndThrow(HttpVersion httpVersion, bo { assertThat(response.getStatus(), is(200)); assertThat(response.getContent(), is("foo")); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); } - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "3")); assertThat(handler.failure(), is(nullValue())); } From 61856eb6d29760572e1b76410447b47bcc883f6e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 22 Jul 2019 14:57:53 +0200 Subject: [PATCH 24/57] Fixed javadoc Signed-off-by: Greg Wilkins --- .../java/org/eclipse/jetty/server/handler/ErrorHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index b4694fd8b4a2..12eb64a84d66 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -183,7 +183,7 @@ protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest req * Accept header, until {@link Request#isHandled()} is true and a * response of the appropriate type is generated. *

- *

The default implementation handles "text/html", "text/*" and "*/*". + *

The default implementation handles "text/html", "text/*" and "*/*". * The method can be overridden to handle other types. Implementations must * immediate produce a response and may not be async. *

From 702967dd79feace1dbd76786e1b8ee50ecf0a985 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 22 Jul 2019 15:00:58 +0200 Subject: [PATCH 25/57] Fixed Javadoc Signed-off-by: Greg Wilkins --- .../src/main/java/org/eclipse/jetty/server/Response.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index f1bc7c6236d8..bb9a12c7108c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -387,12 +387,11 @@ public void sendError(int sc) throws IOException /** * Send an error response. - *

In addition to the servlet standard handling, this method supports some additional codes: + *

In addition to the servlet standard handling, this method supports some additional codes:

*
*
102
Send a partial PROCESSING response and allow additional responses
*
-1
Abort the HttpChannel and close the connection/stream
- * - *

+ *
* @param code The error code * @param message The message * @throws IOException If an IO problem occurred sending the error response. From 802b692878fa7cc216a24634d1993d2d5435dd2d Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 25 Jul 2019 14:57:22 +1000 Subject: [PATCH 26/57] Issue #3806 async sendError - Added resetContent method to leave more non-content headers during sendError - Fixed security tests Signed-off-by: Greg Wilkins --- .../security/SpecExampleConstraintTest.java | 4 +- .../SpnegoAuthenticatorTest.java | 44 +++++++---- .../org/eclipse/jetty/server/HttpChannel.java | 4 +- .../jetty/server/HttpChannelState.java | 4 +- .../org/eclipse/jetty/server/Response.java | 77 +++++++++++++------ .../jetty/server/handler/ErrorHandler.java | 14 +++- .../eclipse/jetty/server/ResponseTest.java | 24 +++++- 7 files changed, 125 insertions(+), 46 deletions(-) diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java index 2f8d243071c3..cd0d46a7a985 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -44,6 +44,7 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -321,7 +322,8 @@ public void testBasic() throws Exception response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); - assertThat(response, startsWith("HTTP/1.1 403 !")); + assertThat(response, startsWith("HTTP/1.1 403 Forbidden")); + assertThat(response, containsString("!Secure")); //a user in role HOMEOWNER can do a GET response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java index 3e771934fa9d..00af87e7ecc7 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.security.authentication; +import java.io.IOException; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; @@ -26,6 +27,7 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.Request; @@ -34,6 +36,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -61,21 +65,27 @@ public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { - return; + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); MetaData.Request metadata = new MetaData.Request(new HttpFields()); metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); @@ -91,17 +101,22 @@ public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { - return; + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); HttpFields http_fields = new HttpFields(); // Create a bogus Authorization header. We don't care about the actual credentials. http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf"); @@ -109,6 +124,7 @@ public void close() metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 96113a6003b2..0e94ad24b5f8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -656,7 +656,7 @@ private void minimalErrorResponse(int code) { try { - _response.reset(true); + _response.resetContent(); _response.setStatus(code); _response.flushBuffer(); _request.setHandled(true); @@ -682,7 +682,7 @@ private void minimalErrorResponse(Throwable failure) code = ((BadMessageException)cause).getCode(); } - _response.reset(true); + _response.resetContent(); _response.setStatus(code); _response.flushBuffer(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 97116805fb9a..5c757229ac59 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -239,7 +239,7 @@ public String getStatusString() /** * @return Next handling of the request should proceed */ - protected Action handling() + public Action handling() { try (Locker.Lock lock = _locker.lock()) { @@ -912,7 +912,7 @@ public void sendError(int code, String message) final Request request = _channel.getRequest(); final Response response = _channel.getResponse(); - response.reset(true); + response.resetContent(); response.getHttpOutput().sendErrorClose(); if (message == null) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index bb9a12c7108c..3c601178a43c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -24,7 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.List; +import java.util.Iterator; import java.util.ListIterator; import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; @@ -1003,14 +1003,7 @@ public void flushBuffer() throws IOException _out.flush(); } - @Override - public void reset() - { - reset(false); - _out.reopen(); - } - - public void reset(boolean preserveCookies) + private void resetStatusAndFields() { _out.resetBuffer(); _outputType = OutputType.NONE; @@ -1021,10 +1014,17 @@ public void reset(boolean preserveCookies) _mimeType = null; _characterEncoding = null; _encodingFrom = EncodingFrom.NOT_SET; + } - List cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; + @Override + public void reset() + { + resetStatusAndFields(); + + // Clear all response headers _fields.clear(); + // recreate necessary connection related fields for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) { HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); @@ -1047,21 +1047,52 @@ public void reset(boolean preserveCookies) } } - if (preserveCookies) - cookies.forEach(_fields::add); - else + // recreate session cookies + Request request = getHttpChannel().getRequest(); + HttpSession session = request.getSession(false); + if (session != null && session.isNew()) { - Request request = getHttpChannel().getRequest(); - HttpSession session = request.getSession(false); - if (session != null && session.isNew()) + SessionHandler sh = request.getSessionHandler(); + if (sh != null) { - SessionHandler sh = request.getSessionHandler(); - if (sh != null) - { - HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); - if (c != null) - addCookie(c); - } + HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); + if (c != null) + addCookie(c); + } + } + } + + public void resetContent() + { + resetStatusAndFields(); + + // remove the content related response headers and keep all others + for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) + { + HttpField field = i.next(); + if (field.getHeader() == null) + continue; + + switch (field.getHeader()) + { + case CONTENT_TYPE: + case CONTENT_LENGTH: + case CONTENT_ENCODING: + case CONTENT_LANGUAGE: + case CONTENT_RANGE: + case CONTENT_MD5: + case CONTENT_LOCATION: + case TRANSFER_ENCODING: + case CACHE_CONTROL: + case LAST_MODIFIED: + case EXPIRES: + case ETAG: + case DATE: + case VARY: + i.remove(); + continue; + default: + continue; } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 12eb64a84d66..0829b5eb6a81 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -35,6 +35,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.QuotedQualityCSV; +import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; @@ -92,7 +93,10 @@ public void doError(String target, Request baseRequest, HttpServletRequest reque if (cacheControl != null) response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl); - generateAcceptableResponse(baseRequest, request, response, response.getStatus(), baseRequest.getResponse().getReason()); + String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE); + if (message == null) + message = baseRequest.getResponse().getReason(); + generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message); } /** @@ -206,6 +210,14 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques case "*/*": { baseRequest.setHandled(true); + /* TODO generate asynchronously ??? + baseRequest.getHttpChannel().sendResponse( + baseRequest.getResponse().getCommittedMetaData(), + generateErrorPageContent(request, code, message), + true, + Callback.from(()->{})); + */ + Writer writer = getAcceptableWriter(baseRequest, request, response); if (writer != null) { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index b4a811556f0b..6ff9f9b3ddab 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -1012,7 +1012,7 @@ public void testAddCookie_JavaNet() throws Exception } @Test - public void testCookiesWithReset() throws Exception + public void testResetContent() throws Exception { Response response = getResponse(); @@ -1028,9 +1028,27 @@ public void testCookiesWithReset() throws Exception cookie2.setPath("/path"); response.addCookie(cookie2); - //keep the cookies - response.reset(true); + response.setContentType("some/type"); + response.setContentLength(3); + response.setHeader(HttpHeader.EXPIRES,"never"); + response.setHeader("SomeHeader", "SomeValue"); + + response.getOutputStream(); + + // reset the content + response.resetContent(); + + // check content is nulled + assertThat(response.getContentType(), nullValue()); + assertThat(response.getContentLength(), is(-1L)); + assertThat(response.getHeader(HttpHeader.EXPIRES.asString()), nullValue()); + response.getWriter(); + + // check arbitrary header still set + assertThat(response.getHeader("SomeHeader"), is("SomeValue")); + + // check cookies are still there Enumeration set = response.getHttpFields().getValues("Set-Cookie"); assertNotNull(set); From f1080a37655e865a3de084325de73f9f5cf8e8f8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 25 Jul 2019 18:07:25 +1000 Subject: [PATCH 27/57] Issue #3806 async sendError - simplified the non dispatch error page writing. Moved towards being able to write async Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 59 ++++------------- .../jetty/server/HttpChannelState.java | 25 ++----- .../org/eclipse/jetty/server/HttpOutput.java | 5 +- .../org/eclipse/jetty/server/Response.java | 19 ++---- .../jetty/server/handler/ErrorHandler.java | 65 ++++++++++++++----- 5 files changed, 72 insertions(+), 101 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 0e94ad24b5f8..7cc820f0a661 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -440,8 +440,8 @@ public boolean handle() errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - minimalErrorResponse(code); _request.setHandled(true); + minimalErrorResponse(code); break; } @@ -486,7 +486,11 @@ public boolean handle() failure = x; else failure.addSuppressed(x); - minimalErrorResponse(failure); + + Throwable cause = unwrap(failure, BadMessageException.class); + int code = cause instanceof BadMessageException ? ((BadMessageException)cause).getCode() : 500; + + minimalErrorResponse(code); } finally { @@ -546,7 +550,7 @@ public boolean handle() break; } } - _response.closeOutput(); + _response.closeOutput(); // TODO make this non blocking! _state.completed(); break; } @@ -575,23 +579,6 @@ public boolean handle() return !suspended; } - protected void sendError(int code, String reason) - { - try - { - _response.sendError(code, reason); - } - catch (Throwable x) - { - if (LOG.isDebugEnabled()) - LOG.debug("Could not send error " + code + " " + reason, x); - } - finally - { - _state.errorComplete(); - } - } - /** *

Sends an error 500, performing a special logic to detect whether the request is suspended, * to avoid concurrent writes from the application.

@@ -658,39 +645,15 @@ private void minimalErrorResponse(int code) { _response.resetContent(); _response.setStatus(code); - _response.flushBuffer(); _request.setHandled(true); - } - catch (Throwable x) - { - abort(x); - } - } - private void minimalErrorResponse(Throwable failure) - { - try - { - int code = 500; - Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - if (status != null) - code = status.intValue(); - else - { - Throwable cause = unwrap(failure, BadMessageException.class); - if (cause instanceof BadMessageException) - code = ((BadMessageException)cause).getCode(); - } - - _response.resetContent(); - _response.setStatus(code); - _response.flushBuffer(); + // TODO use the non blocking version + sendResponse(null, null, true); + _response.getHttpOutput().closed(); } catch (Throwable x) { - if (x != failure) - failure.addSuppressed(x); - abort(failure); + abort(x); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 5c757229ac59..fcb98e61dc9f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -700,21 +700,6 @@ public void complete() runInContext(event, _channel); } - public void errorComplete() - { - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("error complete {}", toStringLocked()); - - _async = Async.COMPLETE; - _event.setDispatchContext(null); - _event.setDispatchPath(null); - } - - cancelTimeout(); - } - public void asyncError(Throwable failure) { // This method is called when an failure occurs asynchronously to @@ -817,6 +802,7 @@ else if (cause instanceof UnavailableException) // error in async with listeners, so give them a chance to handle asyncListeners = _asyncListeners; _async = Async.ERRORING; + _sendError = false; } break; @@ -827,6 +813,7 @@ else if (cause instanceof UnavailableException) } // If we are async and have async listeners + boolean sendErrorCalled = false; if (asyncEvent != null && asyncListeners != null) { // call onError @@ -863,6 +850,8 @@ public String toString() switch (_async) { case ERRORING: + sendErrorCalled = _sendError; + // Still in ERROR state? The listeners did not invoke API methods // and the container must provide a default error dispatch. _async = Async.ERRORED; @@ -890,7 +879,8 @@ public String toString() final Request request = _channel.getRequest(); request.setAttribute(ERROR_EXCEPTION, cause); request.setAttribute(ERROR_EXCEPTION_TYPE, cause.getClass()); - sendError(code, message); + if (!sendErrorCalled) + sendError(code, message); if (dispatch) { @@ -945,9 +935,6 @@ public void sendError(int code, String message) _event.addThrowable(SEND_ERROR_CAUSE); break; - case IDLE: - case COMPLETED: - case UPGRADED: default: { throw new IllegalStateException(getStatusStringLocked()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index c01e62f0500a..762e1da1c9d5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -290,13 +290,12 @@ public void close() } case ASYNC: { - // TODO review this logic? // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. // However it is desirable to allow a close at any time, specially if // complete is called. Thus we simulate a call to isReady here, assuming // that we can transition to READY. - if (!_state.compareAndSet(state, OutputState.READY)) + if (!_state.compareAndSet(state, OutputState.READY))// TODO review this! Why READY? // Should it continue? continue; break; } @@ -347,7 +346,7 @@ public void close() * Called to indicate that the last write has been performed. * It updates the state and performs cleanup operations. */ - void closed() + public void closed() { while (true) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 3c601178a43c..b3f366243534 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -802,21 +802,10 @@ public boolean isContentComplete(long written) public void closeOutput() throws IOException { - switch (_outputType) - { - case WRITER: - _writer.close(); - if (!_out.isClosed()) - _out.close(); - break; - case STREAM: - if (!_out.isClosed()) - getOutputStream().close(); - break; - default: - if (!_out.isClosed()) - _out.close(); - } + if (_outputType == OutputType.WRITER) + _writer.close(); + if (!_out.isClosed()) + _out.close(); } public long getLongContentLength() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 0829b5eb6a81..cd5ede0508d8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; @@ -39,6 +40,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.ByteArrayOutputStream2; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -153,6 +155,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques * @return A {@link Writer} if there is a known acceptable charset or null * @throws IOException if a Writer cannot be returned */ + @Deprecated protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -197,35 +200,65 @@ protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest req * @param response The response (may be wrapped) * @param code the http error code * @param message the http error message - * @param mimeType The mimetype to generate (may be */*or other wildcard) + * @param contentType The mimetype to generate (may be */*or other wildcard) * @throws IOException if a response cannot be generated */ - protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType) + protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType) throws IOException { - switch (mimeType) + switch (contentType) { case "text/html": case "text/*": case "*/*": { - baseRequest.setHandled(true); - /* TODO generate asynchronously ??? - baseRequest.getHttpChannel().sendResponse( - baseRequest.getResponse().getCommittedMetaData(), - generateErrorPageContent(request, code, message), - true, - Callback.from(()->{})); - */ - - Writer writer = getAcceptableWriter(baseRequest, request, response); - if (writer != null) + Charset charset = null; + List acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); + if (acceptable.isEmpty()) + charset = StandardCharsets.ISO_8859_1; + else { - response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); - handleErrorPage(request, writer, code, message); + for (String name : acceptable) + { + if ("*".equals(name)) + { + charset = StandardCharsets.UTF_8; + break; + } + + try + { + charset = Charset.forName(name); + } + catch (Exception e) + { + LOG.ignore(e); + } + } } + + if (charset == null) + return; + ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(bout, charset)); + + baseRequest.setHandled(true); + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + response.setCharacterEncoding(charset.name()); + handleErrorPage(request, writer, code, message); + writer.flush(); + ByteBuffer content = bout.size() == 0 ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(bout.getBuf(), 0, bout.size()); + + // TODO use the non blocking version + baseRequest.getHttpChannel().sendResponse(null, content, true); + baseRequest.getResponse().getHttpOutput().closed(); } + + default: + return; } + + } protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) From 5a21a9d7530b4d13d72ef0cacb0275ac0dc5c48a Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 25 Jul 2019 08:55:43 -0400 Subject: [PATCH 28/57] fixed gzipHandlerTest Signed-off-by: Greg Wilkins --- .../test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java index cf8e7d59261b..1362ac6f4d4f 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerTest.java @@ -153,7 +153,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse response) throw response.setHeader("ETag", __contentETag); String ifnm = req.getHeader("If-None-Match"); if (ifnm != null && ifnm.equals(__contentETag)) - response.sendError(304); + { + response.setStatus(304); + response.flushBuffer(); + } else { PrintWriter writer = response.getWriter(); From 6d9202f0b5c24d3de9f1752e5d5413ee5759660b Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 09:49:57 +1000 Subject: [PATCH 29/57] Issue #3806 async sendError Updated handling of timeout errors. According to servlet spec, exceptions thrown from onTimeout should not be passed to onError, but just logged and ignored: If an exception is thrown while invoking methods in an AsyncListener, it is logged and will not affect the invocation of any other AsyncListeners. This changes several tests. Dispatcher/ContextHandler changes for new ERROR dispatch handling. Feels a bit fragile! Signed-off-by: Greg Wilkins --- .../jetty/server/AsyncContextEvent.java | 2 +- .../org/eclipse/jetty/server/HttpChannel.java | 6 +- .../jetty/server/HttpChannelState.java | 221 ++++++++++-------- .../org/eclipse/jetty/server/Response.java | 5 +- .../jetty/server/handler/ContextHandler.java | 4 +- .../server/HttpManyWaysToAsyncCommitTest.java | 62 +---- .../jetty/servlet/AsyncContextTest.java | 5 +- .../jetty/servlet/AsyncListenerTest.java | 13 +- .../jetty/servlet/AsyncServletTest.java | 43 +--- .../jetty/servlet/CustomRequestLogTest.java | 3 +- 10 files changed, 154 insertions(+), 210 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 0cd93ff67be4..77eb84e4ad36 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -160,7 +160,7 @@ public void run() Scheduler.Task task = _timeoutTask; _timeoutTask = null; if (task != null) - _state.getHttpChannel().execute(() -> _state.onTimeout()); + _state.getHttpChannel().execute(() -> _state.timeout()); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 7cc820f0a661..c611f7c44368 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -418,6 +418,10 @@ public boolean handle() break; } + case ASYNC_TIMEOUT: + _state.onTimeout(); + break; + case ERROR_DISPATCH: { try @@ -612,7 +616,7 @@ else if (noStack != null) LOG.warn(_request.getRequestURI(), failure); } - if (_response.isCommitted()) + if (isCommitted()) abort(failure); else _state.thrownException(failure); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index fcb98e61dc9f..2bc1446af556 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncListener; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; @@ -75,9 +74,10 @@ public enum Action { NOOP, // No action DISPATCH, // handle a normal request dispatch - ASYNC_DISPATCH, // handle an async request dispatch ERROR_DISPATCH, // handle a normal error + ASYNC_DISPATCH, // handle an async request dispatch ASYNC_ERROR, // handle an async error + ASYNC_TIMEOUT, // call asyncContexnt onTimeout WRITE_CALLBACK, // handle an IO write callback READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback @@ -260,9 +260,9 @@ public Action handling() case ASYNC_WOKEN: if (_sendError) { - _sendError = false; _async = Async.NOT_ASYNC; _state = State.DISPATCHED; + _sendError = false; return Action.ERROR_DISPATCH; } @@ -297,21 +297,27 @@ public Action handling() case COMPLETE: _state = State.COMPLETING; return Action.COMPLETE; + case DISPATCH: _state = State.DISPATCHED; _async = Async.NOT_ASYNC; return Action.ASYNC_DISPATCH; + + case EXPIRING: + _state = State.ASYNC_ERROR; + return Action.ASYNC_TIMEOUT; + case EXPIRED: case ERRORED: _state = State.DISPATCHED; _async = Async.NOT_ASYNC; + _sendError = false; return Action.ERROR_DISPATCH; + case STARTED: - case EXPIRING: - case ERRORING: _state = State.ASYNC_WAIT; return Action.NOOP; - case NOT_ASYNC: + default: throw new IllegalStateException(getStatusStringLocked()); } @@ -400,8 +406,8 @@ protected Action unhandle() case COMPLETED: if (_sendError) { - _sendError = false; _state = State.DISPATCHED; + _sendError = false; return Action.ERROR_DISPATCH; } return Action.TERMINATED; @@ -422,8 +428,8 @@ protected Action unhandle() case NOT_ASYNC: if (_sendError) { - _sendError = false; _state = State.DISPATCHED; + _sendError = false; return Action.ERROR_DISPATCH; } _state = State.COMPLETING; @@ -433,8 +439,8 @@ protected Action unhandle() _async = Async.NOT_ASYNC; if (_sendError) { - _sendError = false; _state = State.DISPATCHED; + _sendError = false; return Action.ERROR_DISPATCH; } _state = State.COMPLETING; @@ -487,9 +493,25 @@ protected Action unhandle() } case EXPIRING: - // onTimeout callbacks still being called, so just WAIT - _state = State.ASYNC_WAIT; - return Action.WAIT; + if (_state == State.ASYNC_ERROR) + { + // We must have already called onTimeout and nothing changed, + // so we will do a normal error dispatch + _state = State.DISPATCHED; + _async = Async.NOT_ASYNC; + + final Request request = _channel.getRequest(); + ContextHandler.Context context = _event.getContext(); + if (context != null) + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_STATUS_CODE, 500); + request.setAttribute(ERROR_MESSAGE, "AsyncContext timeout"); + return Action.ERROR_DISPATCH; + } + // onTimeout callbacks need to be called + _state = State.ASYNC_ERROR; + return Action.ASYNC_TIMEOUT; case EXPIRED: case ERRORED: @@ -497,6 +519,7 @@ protected Action unhandle() // we were handling. So do the error dispatch here _state = State.DISPATCHED; _async = Async.NOT_ASYNC; + _sendError = false; return Action.ERROR_DISPATCH; default: @@ -564,10 +587,9 @@ public void dispatch(ServletContext context, String path) scheduleDispatch(); } - protected void onTimeout() + protected void timeout() { - final List listeners; - AsyncContextEvent event; + boolean dispatch = false; try (Locker.Lock lock = _locker.lock()) { if (LOG.isDebugEnabled()) @@ -576,11 +598,36 @@ protected void onTimeout() if (_async != Async.STARTED) return; _async = Async.EXPIRING; + + if (_state == State.ASYNC_WAIT) + { + _state = State.ASYNC_WOKEN; + dispatch = true; + } + } + + if (dispatch) + { + if (LOG.isDebugEnabled()) + LOG.debug("Dispatch after async timeout {}", this); + scheduleDispatch(); + } + } + + protected void onTimeout() + { + final List listeners; + AsyncContextEvent event; + try (Locker.Lock lock = _locker.lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("onTimeout {}", toStringLocked()); + if (_async != Async.EXPIRING || _state != State.ASYNC_ERROR) + throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; } - final AtomicReference error = new AtomicReference<>(); if (listeners != null) { Runnable task = new Runnable() @@ -598,11 +645,6 @@ public void run() { LOG.warn(x + " while invoking onTimeout listener " + listener); LOG.debug(x); - Throwable failure = error.get(); - if (failure == null) - error.set(x); - else if (x != failure) - failure.addSuppressed(x); } } } @@ -616,50 +658,6 @@ public String toString() runInContext(event, task); } - - Throwable th = error.get(); - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case EXPIRING: - _async = th == null ? Async.EXPIRED : Async.ERRORING; - break; - - case COMPLETE: - case DISPATCH: - if (th != null) - { - LOG.ignore(th); - th = null; - } - break; - - default: - throw new IllegalStateException(); - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (th != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Error after async timeout {}", this, th); - thrownException(th); - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after async timeout {}", this); - scheduleDispatch(); - } } public void complete() @@ -674,18 +672,10 @@ public void complete() event = _event; switch (_async) { - case STARTED: - _async = _sendError ? Async.NOT_ASYNC : Async.COMPLETE; - if (_state == State.ASYNC_WAIT) - { - handle = true; - _state = State.ASYNC_WOKEN; - } - break; - case EXPIRING: case ERRORING: - _async = Async.COMPLETE; + case STARTED: + _async = _sendError ? Async.NOT_ASYNC : Async.COMPLETE; break; case COMPLETE: @@ -693,6 +683,11 @@ public void complete() default: throw new IllegalStateException(this.getStatusStringLocked()); } + if (_state == State.ASYNC_WAIT) + { + handle = true; + _state = State.ASYNC_WOKEN; + } } cancelTimeout(event); @@ -706,7 +701,6 @@ public void asyncError(Throwable failure) // normal handling. If the request is async, we arrange for the // exception to be thrown from the normal handling loop and then // actually handled by #thrownException - // TODO can we just directly call thrownException? AsyncContextEvent event = null; try (Locker.Lock lock = _locker.lock()) @@ -755,7 +749,10 @@ protected void thrownException(Throwable th) String message = null; Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); if (cause == null) + { cause = th; + message = th.toString(); + } else if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; @@ -764,38 +761,66 @@ else if (cause instanceof BadMessageException) } else if (cause instanceof UnavailableException) { + message = cause.toString(); if (((UnavailableException)cause).isPermanent()) code = HttpStatus.NOT_FOUND_404; else code = HttpStatus.SERVICE_UNAVAILABLE_503; } - // Check state to see if we are async or not final AsyncContextEvent asyncEvent; final List asyncListeners; - boolean dispatch = false; try (Locker.Lock lock = _locker.lock()) { + // This can only be called from within the handle loop + switch (_state) + { + case ASYNC_WAIT: + case ASYNC_WOKEN: + case IDLE: + throw new IllegalStateException(_state.toString()); + default: + break; + } + + // Check async state to determine type of handling switch (_async) { case NOT_ASYNC: // error not in async, will be handled by error handler in normal handler loop. asyncEvent = null; asyncListeners = null; + if (_sendError) + { + LOG.warn("unhandled due to prior sendError in state " + _async, cause); + return; + } break; + case DISPATCH: + case COMPLETE: + case EXPIRED: + if (_state != State.DISPATCHED) + { + LOG.warn("unhandled in state " + _state + "/" + _async, cause); + return; + } + // Async life cycle method has been correctly called, but not yet acted on + // so we can fall through to same action as STARTED + case STARTED: asyncEvent = _event; + asyncEvent.addThrowable(th); if (_asyncListeners == null || _asyncListeners.isEmpty()) { // error in async, but no listeners so handle it with error dispatch - asyncListeners = null; - _async = Async.ERRORED; - if (_state == State.ASYNC_WAIT) + if (_sendError) { - _state = State.ASYNC_WOKEN; - dispatch = true; + LOG.warn("unhandled due to prior sendError in state " + _async, cause); + return; } + asyncListeners = null; + _async = Async.ERRORED; } else { @@ -813,7 +838,6 @@ else if (cause instanceof UnavailableException) } // If we are async and have async listeners - boolean sendErrorCalled = false; if (asyncEvent != null && asyncListeners != null) { // call onError @@ -847,19 +871,16 @@ public String toString() // check the actions of the listeners try (Locker.Lock lock = _locker.lock()) { + // if anybody has called sendError then we've handled as much as we can by calling listeners + if (_sendError) + return; + switch (_async) { case ERRORING: - sendErrorCalled = _sendError; - - // Still in ERROR state? The listeners did not invoke API methods + // The listeners did not invoke API methods // and the container must provide a default error dispatch. _async = Async.ERRORED; - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } break; case DISPATCH: @@ -879,15 +900,7 @@ public String toString() final Request request = _channel.getRequest(); request.setAttribute(ERROR_EXCEPTION, cause); request.setAttribute(ERROR_EXCEPTION_TYPE, cause.getClass()); - if (!sendErrorCalled) - sendError(code, message); - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after error {}", this); - scheduleDispatch(); - } + sendError(code, message); } public void sendError(int code, String message) @@ -932,7 +945,10 @@ public void sendError(int code, String message) case ASYNC_WAIT: _sendError = true; if (_event != null) - _event.addThrowable(SEND_ERROR_CAUSE); + { + Throwable cause = (Throwable) request.getAttribute(ERROR_EXCEPTION); + _event.addThrowable(cause == null ? SEND_ERROR_CAUSE : cause); + } break; default: @@ -941,7 +957,6 @@ public void sendError(int code, String message) } } } - } protected void completed() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index b3f366243534..e9efeab49660 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -1137,7 +1137,10 @@ public MetaData.Response getCommittedMetaData() @Override public boolean isCommitted() { - return _channel.isCommitted() || _channel.isSendError(); + // If we are in sendError state, we pretend to be committed + if (_channel.isSendError()) + return true; + return _channel.isCommitted(); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index c4625cecf9e2..1696048e1574 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1138,8 +1138,8 @@ public void doScope(String target, Request baseRequest, HttpServletRequest reque if (oldContext != _scontext) { // check the target. - if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || - DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync()) + // TODO this is a fragile accord with the Dispatcher + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) { if (_compactPath) target = URIUtil.compactPath(target); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index c756ceeef61c..b88126734792 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -109,24 +109,7 @@ public void testHandlerDoesNotSetHandledAndThrow(HttpVersion httpVersion, boolea } } - int expected; - if (inWait) - { - // throw happens before async processing, so is handled - expected = 500; - } - else if (dispatch) - { - // throw happen again in async dispatch - expected = 500; - } - else - { - // complete happens before throw, so the throw is ignored and 404 is generated. - expected = 404; - } - - assertThat(response.getStatus(), is(expected)); + assertThat(response.getStatus(), is(500)); assertThat(handler.failure(), is(nullValue())); } @@ -200,10 +183,7 @@ public void testHandlerSetsHandledTrueOnlyAndThrow(HttpVersion httpVersion, bool } } - // If async happens during dispatch it can generate 200 before exception - int expected = inWait ? 500 : (dispatch ? 500 : 200); - - assertThat(response.getStatus(), is(expected)); + assertThat(response.getStatus(), is(500)); assertThat(handler.failure(), is(nullValue())); } @@ -276,23 +256,7 @@ public void testHandlerSetsHandledAndWritesSomeContentAndThrow(HttpVersion httpV } } - if (inWait) - { - // Throw is done before async action and can be handled - assertThat(response.getStatus(), is(500)); - } - else if (dispatch) - { - // async dispatch is thrown again - assertThat(response.getStatus(), is(500)); - } - else - { - // async is done before exception, so exception is not handled - assertThat(response.getStatus(), is(200)); - assertThat(response.getContent(), is("foobar")); - } - + assertThat(response.getStatus(), is(500)); assertThat(handler.failure(), is(nullValue())); } @@ -945,24 +909,12 @@ public void testWriteAndSetContentLengthTooSmallAndThrow(HttpVersion httpVersion response = executeRequest(httpVersion); } - // Setting a content-length too small throws an IllegalStateException - if (inWait) - { - // too late - assertThat(response.getStatus(), is(500)); - assertThat(handler.failure(), is(nullValue())); - } - else if (dispatch) - { - // throw on async dispatch - assertThat(response.getStatus(), is(500)); + assertThat(response.getStatus(), is(500)); + + if (!inWait) assertThat(handler.failure(), not(is(nullValue()))); - } else - { - assertThat(response.getStatus(), is(200)); - assertThat(handler.failure(), not(is(nullValue()))); - } + assertThat(handler.failure(), is(nullValue())); } private class WriteAndSetContentLengthTooSmallHandler extends ThrowExceptionOnDemandHandler diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java index 1ae360927b51..6cc49bd60da9 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java @@ -50,6 +50,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -478,7 +479,7 @@ public void testBadExpire() throws Exception assertThat("error servlet", responseBody, containsString("ERROR: /error")); assertThat("error servlet", responseBody, containsString("PathInfo= /500")); - assertThat("error servlet", responseBody, containsString("EXCEPTION: java.lang.RuntimeException: TEST")); + assertThat("error servlet", responseBody, not(containsString("EXCEPTION: "))); } private class DispatchingRunnable implements Runnable @@ -552,7 +553,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t @Override public void onTimeout(AsyncEvent event) throws IOException { - throw new RuntimeException("TEST"); + throw new RuntimeException("BAD EXPIRE"); } @Override diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java index 50fc3a3f2b1c..4efbb1d2b075 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.QuietServletException; import org.eclipse.jetty.server.Server; @@ -42,6 +43,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; public class AsyncListenerTest @@ -140,7 +142,7 @@ public void test_StartAsync_Throw_OnError_SendError() throws Exception test_StartAsync_Throw_OnError(event -> { HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); - response.sendError(HttpStatus.BAD_GATEWAY_502); + response.sendError(HttpStatus.BAD_GATEWAY_502, "Message!!!"); }); String httpResponse = connector.getResponse( "GET /ctx/path HTTP/1.1\r\n" + @@ -148,7 +150,8 @@ public void test_StartAsync_Throw_OnError_SendError() throws Exception "Connection: close\r\n" + "\r\n"); assertThat(httpResponse, containsString("HTTP/1.1 502 ")); - assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); + assertThat(httpResponse, containsString("Message!!!")); + assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName()))); } @Test @@ -268,7 +271,8 @@ public void test_StartAsync_OnTimeout_Throw() throws Exception "Connection: close\r\n" + "\r\n"); assertThat(httpResponse, containsString("HTTP/1.1 500 ")); - assertThat(httpResponse, containsString(TestRuntimeException.class.getName())); + assertThat(httpResponse, containsString("AsyncContext timeout")); + assertThat(httpResponse, not(containsString(TestRuntimeException.class.getName()))); } @Test @@ -292,6 +296,7 @@ public void test_StartAsync_OnTimeout_SendError() throws Exception { HttpServletResponse response = (HttpServletResponse)event.getAsyncContext().getResponse(); response.sendError(HttpStatus.BAD_GATEWAY_502); + event.getAsyncContext().complete(); }); String httpResponse = connector.getResponse( "GET / HTTP/1.1\r\n" + @@ -447,7 +452,7 @@ public void onTimeout(AsyncEvent event) throws IOException } // Unique named RuntimeException to help during debugging / assertions. - public static class TestRuntimeException extends RuntimeException + public static class TestRuntimeException extends RuntimeException implements QuietException { } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java index d197aa3a57b6..571d8d76a7a5 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java @@ -265,7 +265,6 @@ public void testStartOnTimeoutError() throws Exception "start", "onTimeout", "error", - "onError", "ERROR /ctx/error/custom", "!initial", "onComplete")); @@ -273,44 +272,6 @@ public void testStartOnTimeoutError() throws Exception assertContains("ERROR DISPATCH", response); } - @Test - public void testStartOnTimeoutErrorComplete() throws Exception - { - String response = process("start=200&timeout=error&error=complete", null); - assertThat(response, startsWith("HTTP/1.1 200 OK")); - assertThat(__history, contains( - "REQUEST /ctx/path/info", - "initial", - "start", - "onTimeout", - "error", - "onError", - "complete", - "onComplete")); - - assertContains("COMPLETED", response); - } - - @Test - public void testStartOnTimeoutErrorDispatch() throws Exception - { - String response = process("start=200&timeout=error&error=dispatch", null); - assertThat(response, startsWith("HTTP/1.1 200 OK")); - assertThat(__history, contains( - "REQUEST /ctx/path/info", - "initial", - "start", - "onTimeout", - "error", - "onError", - "dispatch", - "ASYNC /ctx/path/info", - "!initial", - "onComplete")); - - assertContains("DISPATCHED", response); - } - @Test public void testStartOnTimeoutComplete() throws Exception { @@ -526,8 +487,10 @@ public void testStartTimeoutStart() throws Exception "onStartAsync", "start", "onTimeout", + "ERROR /ctx/path/error", + "!initial", "onComplete")); // Error Page Loop! - assertContains("HTTP ERROR 500", response); + assertContains("AsyncContext timeout", response); } @Test diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java index 526a7d8418e5..98b21f004e62 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java @@ -84,7 +84,8 @@ public void testLogFilename() throws Exception _connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n"); String log = _entries.poll(5, TimeUnit.SECONDS); - assertThat(log, is("Filename: " + _tmpDir + File.separator + "servlet" + File.separator + "info")); + String expected = new File(_tmpDir + File.separator + "servlet" + File.separator + "info").getCanonicalPath(); + assertThat(log, is("Filename: " + expected)); } @Test From e448047623a996196777d20b238ae274e6b677e6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 15:56:22 +1000 Subject: [PATCH 30/57] Issue #3806 async sendError Fixed tests in jetty-servlets Signed-off-by: Greg Wilkins --- .../jetty/rewrite/handler/ResponsePatternRule.java | 6 +++++- .../rewrite/handler/ResponsePatternRuleTest.java | 6 ++++-- .../java/org/eclipse/jetty/servlets/DoSFilter.java | 11 ++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index a4b7681d3aca..4f39ad789a25 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; /** @@ -79,7 +80,10 @@ public String apply(String target, HttpServletRequest request, HttpServletRespon // status code 400 and up are error codes if (code >= 400) { - response.sendError(code, _reason); + if (StringUtil.isBlank(_reason)) + response.sendError(code); + else + response.sendError(code, _reason); } else { diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java index aeca52412eda..9321b233a2d8 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java @@ -20,6 +20,8 @@ import java.io.IOException; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Dispatcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -72,7 +74,7 @@ public void testErrorStatusNoReason() throws IOException _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals("", _response.getReason()); + assertEquals(HttpStatus.getMessage(i), _request.getAttribute(Dispatcher.ERROR_MESSAGE)); super.reset(); } } @@ -87,7 +89,7 @@ public void testErrorStatusWithReason() throws IOException _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals("reason-" + i, _response.getReason()); + assertEquals("reason-" + i, _request.getAttribute(Dispatcher.ERROR_MESSAGE)); super.reset(); } } diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java index fcdff1bdec80..d15f57dc3cc7 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java @@ -501,7 +501,16 @@ protected void onRequestTimeout(HttpServletRequest request, HttpServletResponse { if (LOG.isDebugEnabled()) LOG.debug("Timing out {}", request); - response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + try + { + response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + } + catch (IllegalStateException ise) + { + LOG.ignore(ise); + // abort instead + response.sendError(-1); + } } catch (Throwable x) { From 4367f7e5115455d76acecf3a0dafb55e448b3fa3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 16:07:03 +1000 Subject: [PATCH 31/57] Issue #3806 async sendError Fixed tests in jetty-proxy Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/proxy/AbstractProxyServlet.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index 42e5bab92138..41660596c48a 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -675,10 +675,19 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ proxyResponse.resetBuffer(); proxyResponse.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString()); } - proxyResponse.sendError(status); + try + { + proxyResponse.sendError(status); + } + catch (IllegalStateException e) + { + _log.ignore(e); + proxyResponse.sendError(-1); + } } catch (Exception e) { + // Abort connection instead _log.ignore(e); } finally From 3c6cf8ce923e6847852260e27cddfbcdc46651fe Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 17:34:08 +1000 Subject: [PATCH 32/57] more test fixes Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/rewrite/handler/ValidUrlRule.java | 7 ++++++- .../websocket/tests/WebSocketConnectionStatsTest.java | 2 ++ .../jetty/websocket/tests/WebSocketNegotiationTest.java | 2 +- .../websocket/server/WebSocketInvalidVersionTest.java | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java index 2ac00d233d2d..d7210f64b59a 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java @@ -22,6 +22,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -88,11 +90,14 @@ public String matchAndApply(String target, HttpServletRequest request, HttpServl // status code 400 and up are error codes so include a reason if (code >= 400) { - response.sendError(code, _reason); + response.sendError(code); + if (!StringUtil.isBlank(_reason)) + Request.getBaseRequest(request).getResponse().setStatusWithReason(code, _reason); } else { response.setStatus(code); + response.flushBuffer(); } // we have matched, return target and consider it is handled diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java index cb25fa4d1f16..a35087ff6e85 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java @@ -46,6 +46,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -121,6 +122,7 @@ long getFrameByteSize(WebSocketFrame frame) } @Test + @Disabled // TODO this is a flakey test public void echoStatsTest() throws Exception { URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/testPath"); diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java index 9c23e097ccb5..27ec3c2c45ab 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketNegotiationTest.java @@ -115,7 +115,7 @@ public void testInvalidUpgradeRequestNoKey() throws Exception client.getOutputStream().write(upgradeRequest.getBytes(ISO_8859_1)); String response = getUpgradeResponse(client.getInputStream()); - assertThat(response, containsString("400 Missing request header 'Sec-WebSocket-Key'")); + assertThat(response, containsString("400 ")); } protected static HttpFields newUpgradeRequest(String extensions) diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java index 105de580dc21..36989de46073 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketInvalidVersionTest.java @@ -89,6 +89,6 @@ public void testRequestVersion29() throws Exception connFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT); }); assertThat(x.getCause(), instanceOf(UpgradeException.class)); - assertThat(x.getMessage(), containsString("400 Unsupported websocket version specification")); + assertThat(x.getMessage(), containsString("400 ")); } } From 2558aac086f86122ff9fd25ba0336af147215d15 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 17:52:29 +1000 Subject: [PATCH 33/57] Issue #3806 async sendError Fixed head handling Signed-off-by: Greg Wilkins --- .../src/main/java/org/eclipse/jetty/server/HttpChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index c611f7c44368..4f8111c6409c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -544,7 +544,7 @@ public boolean handle() } // RFC 7230, section 3.3. - if (!_response.isContentComplete(_response.getHttpOutput().getWritten())) + if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) { if (isCommitted()) abort(new IOException("insufficient content written")); From 890f59c3321545cfc8408c527e6f1aebf7a9f293 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 09:24:25 -0400 Subject: [PATCH 34/57] Issue #3804 CDI integration reverted HttpMethod fromString Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/client/ConnectionPoolTest.java | 2 +- .../main/java/org/eclipse/jetty/http/HttpMethod.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index d099a733d286..79942ba172b5 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -120,7 +120,7 @@ public void test(Class connectionPoolClass, Connection @Override protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - switch (HttpMethod.valueOf(request.getMethod())) + switch (HttpMethod.fromString(request.getMethod())) { case GET: { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java index 930035d5092d..741be954df55 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java @@ -180,4 +180,14 @@ public String asString() return toString(); } + /** + * Converts the given String parameter to an HttpMethod + * + * @param method the String to get the equivalent HttpMethod from + * @return the HttpMethod or null if the parameter method is unknown + */ + public static HttpMethod fromString(String method) + { + return CACHE.get(method); + } } From ca5a07ae39e7566ff9d3aebe7e78c149d2a82d0d Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 27 Jul 2019 10:22:51 -0400 Subject: [PATCH 35/57] Issue #3804 CDI integration reverted unnecessary changes Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/http/HttpGenerator.java | 11 ++++++----- .../jetty/proxy/AbstractProxyServlet.java | 2 +- .../rewrite/handler/ResponsePatternRule.java | 6 +----- .../jetty/rewrite/handler/ValidUrlRule.java | 1 - .../eclipse/jetty/server/HttpChannelState.java | 2 +- .../jetty/server/handler/ContextHandler.java | 16 ++++------------ .../jetty/server/handler/ErrorHandler.java | 2 -- .../jetty/util/thread/QueuedThreadPool.java | 3 ++- 8 files changed, 15 insertions(+), 28 deletions(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index dec9094b7665..e23bb1bad04c 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -51,12 +51,13 @@ public class HttpGenerator private static final byte[] __colon_space = new byte[]{':', ' '}; public static final MetaData.Response CONTINUE_100_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 100, null, null, -1); public static final MetaData.Response PROGRESS_102_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 102, null, null, -1); - public static final MetaData.Response RESPONSE_500_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields() - { + public static final MetaData.Response RESPONSE_500_INFO = + new MetaData.Response(HttpVersion.HTTP_1_1, INTERNAL_SERVER_ERROR_500, null, new HttpFields() { - put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); - } - }, 0); + { + put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); + } + }, 0); // states public enum State diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index 41660596c48a..c2c45ace64ca 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -682,12 +682,12 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ catch (IllegalStateException e) { _log.ignore(e); + // Abort connection instead proxyResponse.sendError(-1); } } catch (Exception e) { - // Abort connection instead _log.ignore(e); } finally diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index 4f39ad789a25..a4b7681d3aca 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -22,7 +22,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; /** @@ -80,10 +79,7 @@ public String apply(String target, HttpServletRequest request, HttpServletRespon // status code 400 and up are error codes if (code >= 400) { - if (StringUtil.isBlank(_reason)) - response.sendError(code); - else - response.sendError(code, _reason); + response.sendError(code, _reason); } else { diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java index d7210f64b59a..3ff48c8dcd63 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java @@ -97,7 +97,6 @@ public String matchAndApply(String target, HttpServletRequest request, HttpServl else { response.setStatus(code); - response.flushBuffer(); } // we have matched, return target and consider it is handled diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 2bc1446af556..da328cda1e8f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -74,8 +74,8 @@ public enum Action { NOOP, // No action DISPATCH, // handle a normal request dispatch - ERROR_DISPATCH, // handle a normal error ASYNC_DISPATCH, // handle an async request dispatch + ERROR_DISPATCH, // handle a normal error ASYNC_ERROR, // handle an async error ASYNC_TIMEOUT, // call asyncContexnt onTimeout WRITE_CALLBACK, // handle an IO write callback diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 1696048e1574..1c07b18aa49a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1276,19 +1276,11 @@ public void doHandle(String target, Request baseRequest, HttpServletRequest requ if (new_context) requestInitialized(baseRequest, request); - switch (dispatch) + if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target)) { - case REQUEST: - if (isProtectedTarget(target)) - { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - baseRequest.setHandled(true); - return; - } - break; - - default: - break; + response.sendError(HttpServletResponse.SC_NOT_FOUND); + baseRequest.setHandled(true); + return; } nextHandle(target, baseRequest, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index cd5ede0508d8..9ca5aaaf14ae 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -257,8 +257,6 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques default: return; } - - } protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index c987f4308282..3f5addd27bba 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -187,12 +187,13 @@ protected void doStop() throws Exception // Consume any reserved threads while (tryExecute(NOOP)) { - ; } // Fill the job queue with noop jobs to wakeup idle threads. for (int i = 0; i < threads; ++i) + { jobs.offer(NOOP); + } // try to let jobs complete naturally for half our stop time joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2); From 001e219e51229eb5734144c9822b604e1fdc1d4c Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sun, 28 Jul 2019 07:20:26 +1000 Subject: [PATCH 36/57] Issue #3806 async sendError Improved reason handling Signed-off-by: Greg Wilkins --- .../rewrite/handler/ResponsePatternRule.java | 15 +++++++++++++-- .../rewrite/handler/ResponsePatternRuleTest.java | 2 +- .../java/org/eclipse/jetty/server/Response.java | 5 ++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index a4b7681d3aca..c7baa33a516f 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -22,6 +22,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; /** @@ -79,11 +81,20 @@ public String apply(String target, HttpServletRequest request, HttpServletRespon // status code 400 and up are error codes if (code >= 400) { - response.sendError(code, _reason); + if (!StringUtil.isBlank(_reason)) + { + // use both setStatusWithReason (to set the reason) and sendError to set the message + Request.getBaseRequest(request).getResponse().setStatusWithReason(code, _reason); + response.sendError(code, _reason); + } + else + { + response.sendError(code); + } } else { - response.setStatus(code); + response.setStatus(code, _reason); } return target; } diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java index 9321b233a2d8..fd6c1290b9b3 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRuleTest.java @@ -61,7 +61,7 @@ public void testStatusCodeWithReason() throws IOException _rule.apply(null, _request, _response); assertEquals(i, _response.getStatus()); - assertEquals(null, _response.getReason()); + assertEquals("reason" + i, _response.getReason()); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index e9efeab49660..e43eec2b6fe3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -625,8 +625,11 @@ public void setStatus(int sc) throw new IllegalArgumentException(); if (!isIncluding()) { + // Null the reason only if the status is different. This allows + // a specific reason to be sent with setStatusWithReason followed by sendError. + if (_status != sc) + _reason = null; _status = sc; - _reason = null; } } From f6ef8ac8b9e0e86ed4f09d87de4882c2bab9b7d4 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 29 Jul 2019 17:45:31 +1000 Subject: [PATCH 37/57] Issue #3806 async sendError WIP on fully async error handling. Simplified HttpChannelState state machines to allow for async actions during completing Signed-off-by: Greg Wilkins --- .../jetty/server/AsyncContextEvent.java | 2 +- .../org/eclipse/jetty/server/HttpChannel.java | 56 +- .../jetty/server/HttpChannelState.java | 1057 ++++++++--------- .../eclipse/jetty/server/HttpConnection.java | 13 +- .../org/eclipse/jetty/server/HttpOutput.java | 56 +- .../jetty/server/handler/ErrorHandler.java | 64 +- .../server/HttpManyWaysToAsyncCommitTest.java | 4 +- .../jetty/server/LocalAsyncContextTest.java | 5 + .../eclipse/jetty/server/ResponseTest.java | 2 +- .../server/handler/NcsaRequestLogTest.java | 11 +- .../eclipse/jetty/servlet/ErrorPageTest.java | 2 +- 11 files changed, 658 insertions(+), 614 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 77eb84e4ad36..52520ffd71f4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -160,7 +160,7 @@ public void run() Scheduler.Task task = _timeoutTask; _timeoutTask = null; if (task != null) - _state.getHttpChannel().execute(() -> _state.timeout()); + _state.timeout(); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 4f8111c6409c..f5a6b130da28 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -71,8 +70,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { private static final Logger LOG = Log.getLogger(HttpChannel.class); - private final AtomicBoolean _committed = new AtomicBoolean(); - private final AtomicBoolean _responseCompleted = new AtomicBoolean(); private final AtomicLong _requests = new AtomicLong(); private final Connector _connector; private final Executor _executor; @@ -289,8 +286,6 @@ public void continue100(int available) throws IOException public void recycle() { - _committed.set(false); - _responseCompleted.set(false); _request.recycle(); _response.recycle(); _committedMetaData = null; @@ -325,7 +320,7 @@ public void run() public boolean handle() { if (LOG.isDebugEnabled()) - LOG.debug("{} handle {} ", this, _request.getHttpURI()); + LOG.debug("handle {} {} ", _request.getHttpURI(), this); HttpChannelState.Action action = _state.handling(); @@ -339,7 +334,7 @@ public boolean handle() try { if (LOG.isDebugEnabled()) - LOG.debug("{} action {}", this, action); + LOG.debug("action {} {}", action, this); switch (action) { @@ -494,7 +489,8 @@ public boolean handle() Throwable cause = unwrap(failure, BadMessageException.class); int code = cause instanceof BadMessageException ? ((BadMessageException)cause).getCode() : 500; - minimalErrorResponse(code); + if (!_state.isResponseCommitted()) + minimalErrorResponse(code); } finally { @@ -561,7 +557,7 @@ public boolean handle() default: { - throw new IllegalStateException("state=" + _state); + throw new IllegalStateException(this.toString()); } } } @@ -577,7 +573,7 @@ public boolean handle() } if (LOG.isDebugEnabled()) - LOG.debug("{} handle exit, result {}", this, action); + LOG.debug("!handle {} {}", action, this); boolean suspended = action == Action.WAIT; return !suspended; @@ -607,7 +603,7 @@ else if (noStack != null) { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) - LOG.debug(_request.getRequestURI(), failure); + LOG.warn(_request.getRequestURI(), failure); else LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); } @@ -653,7 +649,6 @@ private void minimalErrorResponse(int code) // TODO use the non blocking version sendResponse(null, null, true); - _response.getHttpOutput().closed(); } catch (Throwable x) { @@ -679,7 +674,7 @@ public String toString() getClass().getSimpleName(), hashCode(), _requests, - _committed.get(), + _state.isResponseCommitted(), isRequestCompleted(), isResponseCompleted(), _state.getState(), @@ -716,7 +711,7 @@ public void onRequest(MetaData.Request request) public boolean onContent(HttpInput.Content content) { if (LOG.isDebugEnabled()) - LOG.debug("{} onContent {}", this, content); + LOG.debug("onContent {} {}", this, content); notifyRequestContent(_request, content.getByteBuffer()); return _request.getHttpInput().addContent(content); } @@ -724,7 +719,7 @@ public boolean onContent(HttpInput.Content content) public boolean onContentComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onContentComplete", this); + LOG.debug("onContentComplete {}", this); notifyRequestContentEnd(_request); return false; } @@ -732,7 +727,7 @@ public boolean onContentComplete() public void onTrailers(HttpFields trailers) { if (LOG.isDebugEnabled()) - LOG.debug("{} onTrailers {}", this, trailers); + LOG.debug("onTrailers {} {}", this, trailers); _trailers = trailers; notifyRequestTrailers(_request); } @@ -740,7 +735,7 @@ public void onTrailers(HttpFields trailers) public boolean onRequestComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onRequestComplete", this); + LOG.debug("onRequestComplete {}", this); boolean result = _request.getHttpInput().eof(); notifyRequestEnd(_request); return result; @@ -822,9 +817,9 @@ public void onBadMessage(BadMessageException failure) } } - protected boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback) + public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean complete, final Callback callback) { - boolean committing = _committed.compareAndSet(false, true); + boolean committing = _state.commitResponse(); if (LOG.isDebugEnabled()) LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", @@ -890,7 +885,7 @@ protected void commit(MetaData.Response info) public boolean isCommitted() { - return _committed.get(); + return _state.isResponseCommitted(); } /** @@ -906,7 +901,7 @@ public boolean isRequestCompleted() */ public boolean isResponseCompleted() { - return _responseCompleted.get(); + return _state.isResponseCompleted(); } public boolean isPersistent() @@ -969,8 +964,11 @@ public boolean useDirectBuffers() */ public void abort(Throwable failure) { - notifyResponseFailure(_request, failure); - _transport.abort(failure); + if (_state.abortResponse()) + { + notifyResponseFailure(_request, failure); + _transport.abort(failure); + } } private void notifyRequestBegin(Request request) @@ -1279,16 +1277,15 @@ private SendCallback(Callback callback, ByteBuffer content, boolean commit, bool public void succeeded() { _written += _length; + if (_complete) + _response.getHttpOutput().closed(); super.succeeded(); if (_commit) notifyResponseCommit(_request); if (_length > 0) notifyResponseContent(_request, _content); - if (_complete) - { - _responseCompleted.set(true); + if (_complete && _state.completeResponse()) notifyResponseEnd(_request); - } } @Override @@ -1304,13 +1301,14 @@ public void failed(final Throwable x) @Override public void succeeded() { - super.failed(x); _response.getHttpOutput().closed(); + super.failed(x); } @Override public void failed(Throwable th) { + _response.getHttpOutput().closed(); abort(x); super.failed(x); } @@ -1334,7 +1332,7 @@ private Send100Callback(Callback callback) @Override public void succeeded() { - if (_committed.compareAndSet(true, false)) + if (_state.partialResponse()) super.succeeded(); else super.failed(new IllegalStateException()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index da328cda1e8f..7010e988a7a8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -57,14 +57,44 @@ public class HttpChannelState */ public enum State { - IDLE, // Idle request - DISPATCHED, // Request dispatched to filter/servlet or Async IO callback - ASYNC_WAIT, // Suspended and waiting - ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT - ASYNC_ERROR, // Async error from ASYNC_WAIT + IDLE, // Idle request + HANDLING, // Request dispatched to filter/servlet or Async IO callback + WAITING, // Suspended and waiting + WAKING, // Dispatch to handle from ASYNC_WAIT + UPGRADED // Request upgraded the connection + } + + /** + * The state of the servlet async API. + */ + private enum LifeCycleState + { + BLOCKING, + ASYNC, // AsyncContext.startAsync() has been called + DISPATCH, // AsyncContext.dispatch() has been called + EXPIRE, // AsyncContext timeout has happened + EXPIRING, // AsyncListeners are being called + COMPLETE, // AsyncContext.complete() has been called COMPLETING, // Response is completable - COMPLETED, // Response is completed - UPGRADED // Request upgraded the connection + COMPLETED // Response is completed + } + + private enum AsyncReadState + { + IDLE, // No isReady; No data + REGISTER, // isReady()==false handling; No data + REGISTERED, // isReady()==false !handling; No data + POSSIBLE, // isReady()==false async read callback called (http/1 only) + PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) + READY // isReady() was false, onContentAdded has been called + } + + private enum ResponseState + { + OPEN, + COMMITTED, + COMPLETED, + ABORTED, } /** @@ -77,7 +107,7 @@ public enum Action ASYNC_DISPATCH, // handle an async request dispatch ERROR_DISPATCH, // handle a normal error ASYNC_ERROR, // handle an async error - ASYNC_TIMEOUT, // call asyncContexnt onTimeout + ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback @@ -86,40 +116,14 @@ public enum Action WAIT, // Wait for further events } - /** - * The state of the servlet async API. - */ - private enum Async - { - NOT_ASYNC, - STARTED, // AsyncContext.startAsync() has been called - DISPATCH, // AsyncContext.dispatch() has been called - COMPLETE, // AsyncContext.complete() has been called - EXPIRING, // AsyncContext timeout just happened - EXPIRED, // AsyncContext timeout has been processed - ERRORING, // An error just happened - ERRORED // The error has been processed - } - - private enum AsyncRead - { - IDLE, // No isReady; No data - REGISTER, // isReady()==false handling; No data - REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async read callback called (http/1 only) - PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) - READY // isReady() was false, onContentAdded has been called - } - - private static final Throwable SEND_ERROR_CAUSE = new Throwable("SEND_ERROR_CAUSE"); - private final Locker _locker = new Locker(); private final HttpChannel _channel; private List _asyncListeners; private State _state; - private Async _async; + private LifeCycleState _lifeCycleState; + private ResponseState _responseState; + private AsyncReadState _asyncReadState = AsyncReadState.IDLE; private boolean _initial; - private AsyncRead _asyncRead = AsyncRead.IDLE; private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; @@ -129,13 +133,14 @@ protected HttpChannelState(HttpChannel channel) { _channel = channel; _state = State.IDLE; - _async = Async.NOT_ASYNC; + _lifeCycleState = LifeCycleState.BLOCKING; + _responseState = ResponseState.OPEN; _initial = true; } public State getState() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state; } @@ -143,7 +148,7 @@ public State getState() public void addListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) _asyncListeners = new ArrayList<>(); @@ -153,7 +158,7 @@ public void addListener(AsyncListener listener) public boolean hasListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) return false; @@ -172,7 +177,7 @@ public boolean hasListener(AsyncListener listener) public boolean isSendError() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _sendError; } @@ -180,7 +185,7 @@ public boolean isSendError() public void setTimeout(long ms) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { _timeoutMs = ms; } @@ -188,7 +193,7 @@ public void setTimeout(long ms) public long getTimeout() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _timeoutMs; } @@ -196,7 +201,7 @@ public long getTimeout() public AsyncContextEvent getAsyncContextEvent() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _event; } @@ -205,7 +210,7 @@ public AsyncContextEvent getAsyncContextEvent() @Override public String toString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return toStringLocked(); } @@ -213,35 +218,131 @@ public String toString() public String toStringLocked() { - return String.format("%s@%x{s=%s a=%s i=%b r=%s w=%b}", + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), - _state, - _async, - _initial, - _asyncRead, - _asyncWritePossible); + getStatusStringLocked()); } private String getStatusStringLocked() { - return String.format("s=%s i=%b a=%s", _state, _initial, _async); + return String.format("s=%s lc=%s rs=%s ars=%s awp=%b se=%b i=%b", + _state, + _lifeCycleState, + _responseState, + _asyncReadState, + _asyncWritePossible, + _sendError, + _initial); } public String getStatusString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return getStatusStringLocked(); } } + public boolean commitResponse() + { + synchronized (this) + { + switch(_responseState) + { + case OPEN: + _responseState = ResponseState.COMMITTED; + return true; + + default: + return false; + } + } + } + + public boolean partialResponse() + { + synchronized (this) + { + switch(_responseState) + { + case COMMITTED: + _responseState = ResponseState.OPEN; + return true; + + default: + return false; + } + } + } + + public boolean completeResponse() + { + synchronized (this) + { + switch(_responseState) + { + case OPEN: + case COMMITTED: + _responseState = ResponseState.COMPLETED; + return true; + + default: + return false; + } + } + } + + public boolean isResponseCommitted() + { + synchronized (this) + { + switch (_responseState) + { + case OPEN: + return false; + default: + return true; + } + } + } + + public boolean isResponseCompleted() + { + synchronized (this) + { + return _responseState == ResponseState.COMPLETED; + } + } + + public boolean abortResponse() + { + synchronized (this) + { + switch(_responseState) + { + case ABORTED: + return false; + case OPEN: + // No response has been committed + // TODO we need a better way to signal to the request log that an abort was done + _channel.getResponse().setStatus(500); + _responseState = ResponseState.ABORTED; + return true; + + default: + _responseState = ResponseState.ABORTED; + return true; + } + } + } + /** * @return Next handling of the request should proceed */ public Action handling() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("handling {}", toStringLocked()); @@ -249,103 +350,203 @@ public Action handling() switch (_state) { case IDLE: + if (_lifeCycleState != LifeCycleState.BLOCKING) + throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = State.DISPATCHED; + _state = State.HANDLING; return Action.DISPATCH; - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; + case WAKING: + if (_event != null && _event.getThrowable() != null && !_sendError) + return Action.ASYNC_ERROR; - case ASYNC_WOKEN: - if (_sendError) - { - _async = Async.NOT_ASYNC; - _state = State.DISPATCHED; - _sendError = false; - return Action.ERROR_DISPATCH; - } + Action action = nextAction(true); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(true) {} {}", action, toStringLocked()); + return action; - switch (_asyncRead) + case WAITING: + case HANDLING: + case UPGRADED: + default: + throw new IllegalStateException(getStatusStringLocked()); + } + } + } + + /** + * Signal that the HttpConnection has finished handling the request. + * For blocking connectors, this call may block if the request has + * been suspended (startAsync called). + * + * @return next actions + * be handled again (eg because of a resume that happened before unhandle was called) + */ + protected Action unhandle() + { + boolean readInterested = false; + + synchronized (this) + { + try + { + if (LOG.isDebugEnabled()) + LOG.debug("unhandle {}", toStringLocked()); + + switch (_state) + { + case HANDLING: + break; + + default: + throw new IllegalStateException(this.getStatusStringLocked()); + } + + _initial = false; + + Action action = nextAction(false); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(false) {} {}", action, toStringLocked()); + return action; + } + finally + { + if (_state == State.WAITING) + { + switch(_asyncReadState) { - case POSSIBLE: - _state = State.DISPATCHED; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - case READY: - _state = State.DISPATCHED; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; case REGISTER: case PRODUCING: - case IDLE: - case REGISTERED: - break; + _channel.onAsyncWaitForContent(); default: - throw new IllegalStateException(getStatusStringLocked()); + break; } + } + } + } + } - if (_asyncWritePossible) - { - _state = State.DISPATCHED; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } + private Action nextAction(boolean handling) + { + if (_sendError) + { + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.BLOCKING; + _sendError = false; + return Action.ERROR_DISPATCH; + } - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - return Action.COMPLETE; - - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - - case EXPIRING: - _state = State.ASYNC_ERROR; - return Action.ASYNC_TIMEOUT; - - case EXPIRED: - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - _sendError = false; - return Action.ERROR_DISPATCH; - - case STARTED: - _state = State.ASYNC_WAIT; - return Action.NOOP; + switch (_lifeCycleState) + { + case BLOCKING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.COMPLETING; + return Action.COMPLETE; - default: - throw new IllegalStateException(getStatusStringLocked()); - } + case ASYNC: + switch (_asyncReadState) + { + case POSSIBLE: + _state = State.HANDLING; + _asyncReadState = AsyncReadState.PRODUCING; + return Action.READ_PRODUCE; + case READY: + _state = State.HANDLING; + _asyncReadState = AsyncReadState.IDLE; + return Action.READ_CALLBACK; + case REGISTER: + case PRODUCING: + case IDLE: + case REGISTERED: + break; + default: + throw new IllegalStateException(getStatusStringLocked()); + } - case ASYNC_ERROR: - return Action.ASYNC_ERROR; + if (_asyncWritePossible) + { + _state = State.HANDLING; + _asyncWritePossible = false; + return Action.WRITE_CALLBACK; + } - case ASYNC_WAIT: - case DISPATCHED: - case UPGRADED: - default: + if (handling) throw new IllegalStateException(getStatusStringLocked()); - } + + Scheduler scheduler = _channel.getScheduler(); + if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) + _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); + _state = State.WAITING; + return Action.WAIT; + + case DISPATCH: + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.BLOCKING; + return Action.ASYNC_DISPATCH; + + case EXPIRE: + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.EXPIRING; + return Action.ASYNC_TIMEOUT; + + case EXPIRING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + + // We must have already called onTimeout and nothing changed, + // so we will do a normal error dispatch + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.BLOCKING; + + final Request request = _channel.getRequest(); + ContextHandler.Context context = _event.getContext(); + if (context != null) + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_STATUS_CODE, 500); + request.setAttribute(ERROR_MESSAGE, "AsyncContext timeout"); + return Action.ERROR_DISPATCH; + + case COMPLETE: + _state = State.HANDLING; + _lifeCycleState = LifeCycleState.COMPLETING; + return Action.COMPLETE; + + case COMPLETING: + if (handling) + { + _state = State.HANDLING; + return Action.COMPLETE; + } + + _state = State.WAITING; + return Action.WAIT; + + case COMPLETED: + _state = State.IDLE; + _lifeCycleState = LifeCycleState.COMPLETED; + return Action.TERMINATED; + + default: + throw new IllegalStateException(getStatusStringLocked()); } } + public void startAsync(AsyncContextEvent event) { final List lastAsyncListeners; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != State.DISPATCHED || _async != Async.NOT_ASYNC) + if (_state != State.HANDLING || _lifeCycleState != LifeCycleState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); - _async = Async.STARTED; + _lifeCycleState = LifeCycleState.ASYNC; _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; @@ -383,181 +584,30 @@ public String toString() } } - /** - * Signal that the HttpConnection has finished handling the request. - * For blocking connectors, this call may block if the request has - * been suspended (startAsync called). - * - * @return next actions - * be handled again (eg because of a resume that happened before unhandle was called) - */ - protected Action unhandle() - { - boolean readInterested = false; - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("unhandle {}", toStringLocked()); - - switch (_state) - { - case COMPLETING: - case COMPLETED: - if (_sendError) - { - _state = State.DISPATCHED; - _sendError = false; - return Action.ERROR_DISPATCH; - } - return Action.TERMINATED; - - case DISPATCHED: - case ASYNC_ERROR: - case ASYNC_WAIT: - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } - - _initial = false; - - switch (_async) - { - case NOT_ASYNC: - if (_sendError) - { - _state = State.DISPATCHED; - _sendError = false; - return Action.ERROR_DISPATCH; - } - _state = State.COMPLETING; - return Action.COMPLETE; - - case COMPLETE: - _async = Async.NOT_ASYNC; - if (_sendError) - { - _state = State.DISPATCHED; - _sendError = false; - return Action.ERROR_DISPATCH; - } - _state = State.COMPLETING; - return Action.COMPLETE; - - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - - case STARTED: - switch (_asyncRead) - { - case READY: - _state = State.DISPATCHED; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - - case POSSIBLE: - _state = State.DISPATCHED; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - - case REGISTER: - case PRODUCING: - _asyncRead = AsyncRead.REGISTERED; - readInterested = true; - break; - - case IDLE: - case REGISTERED: - break; - } - - if (_asyncWritePossible) - { - _state = State.DISPATCHED; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } - else - { - _state = State.ASYNC_WAIT; - - Scheduler scheduler = _channel.getScheduler(); - if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) - _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - - return Action.WAIT; - } - - case EXPIRING: - if (_state == State.ASYNC_ERROR) - { - // We must have already called onTimeout and nothing changed, - // so we will do a normal error dispatch - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - - final Request request = _channel.getRequest(); - ContextHandler.Context context = _event.getContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_STATUS_CODE, 500); - request.setAttribute(ERROR_MESSAGE, "AsyncContext timeout"); - return Action.ERROR_DISPATCH; - } - // onTimeout callbacks need to be called - _state = State.ASYNC_ERROR; - return Action.ASYNC_TIMEOUT; - - case EXPIRED: - case ERRORED: - // onTimeout or onError handling is complete, but did not dispatch as - // we were handling. So do the error dispatch here - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - _sendError = false; - return Action.ERROR_DISPATCH; - - default: - _state = State.COMPLETING; - return Action.COMPLETE; - } - } - finally - { - if (readInterested) - _channel.onAsyncWaitForContent(); - } - } - public void dispatch(ServletContext context, String path) { boolean dispatch = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("dispatch {} -> {}", toStringLocked(), path); + // TODO this method can be simplified + boolean started = false; event = _event; - switch (_async) + switch (_lifeCycleState) { - case STARTED: + case ASYNC: started = true; break; case EXPIRING: - case ERRORING: - case ERRORED: break; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.DISPATCH; + _lifeCycleState = LifeCycleState.DISPATCH; if (context != null) _event.setDispatchContext(context); @@ -568,11 +618,11 @@ public void dispatch(ServletContext context, String path) { switch (_state) { - case DISPATCHED: - case ASYNC_WOKEN: + case HANDLING: + case WAKING: break; - case ASYNC_WAIT: - _state = State.ASYNC_WOKEN; + case WAITING: + _state = State.WAKING; dispatch = true; break; default: @@ -590,18 +640,18 @@ public void dispatch(ServletContext context, String path) protected void timeout() { boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("onTimeout {}", toStringLocked()); + LOG.debug("Timeout {}", toStringLocked()); - if (_async != Async.STARTED) + if (_lifeCycleState != LifeCycleState.ASYNC) return; - _async = Async.EXPIRING; + _lifeCycleState = LifeCycleState.EXPIRE; - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { - _state = State.ASYNC_WOKEN; + _state = State.WAKING; dispatch = true; } } @@ -618,11 +668,11 @@ protected void onTimeout() { final List listeners; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - if (_async != Async.EXPIRING || _state != State.ASYNC_ERROR) + if (_lifeCycleState != LifeCycleState.EXPIRING || _state != State.HANDLING) throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; @@ -664,18 +714,17 @@ public void complete() { boolean handle = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("complete {}", toStringLocked()); event = _event; - switch (_async) + switch (_lifeCycleState) { case EXPIRING: - case ERRORING: - case STARTED: - _async = _sendError ? Async.NOT_ASYNC : Async.COMPLETE; + case ASYNC: + _lifeCycleState = _sendError ? LifeCycleState.BLOCKING : LifeCycleState.COMPLETE; break; case COMPLETE: @@ -683,10 +732,10 @@ public void complete() default: throw new IllegalStateException(this.getStatusStringLocked()); } - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { handle = true; - _state = State.ASYNC_WOKEN; + _state = State.WAKING; } } @@ -703,31 +752,17 @@ public void asyncError(Throwable failure) // actually handled by #thrownException AsyncContextEvent event = null; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - switch (_state) + if (_state == State.WAITING && _lifeCycleState == LifeCycleState.ASYNC) { - case IDLE: - case DISPATCHED: - case COMPLETING: - case COMPLETED: - case UPGRADED: - case ASYNC_WOKEN: - case ASYNC_ERROR: - { - break; - } - case ASYNC_WAIT: - { - _event.addThrowable(failure); - _state = State.ASYNC_ERROR; - event = _event; - break; - } - default: - { - throw new IllegalStateException(getStatusStringLocked()); - } + _event.addThrowable(failure); + event = _event; + } + else + { + LOG.warn(failure.toString()); + LOG.debug(failure); } } @@ -744,163 +779,136 @@ protected void thrownException(Throwable th) // + If the request is async, then any async listeners are give a chance to handle the exception in their onError handler. // + If the request is not async, or not handled by any async onError listener, then a normal sendError is done. - // Determine the actual details of the exception - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String message = null; - Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - if (cause == null) - { - cause = th; - message = th.toString(); - } - else if (cause instanceof BadMessageException) + Runnable sendError = () -> { - BadMessageException bme = (BadMessageException)cause; - code = bme.getCode(); - message = bme.getReason(); - } - else if (cause instanceof UnavailableException) - { - message = cause.toString(); - if (((UnavailableException)cause).isPermanent()) - code = HttpStatus.NOT_FOUND_404; + final Request request = _channel.getRequest(); + + // Determine the actual details of the exception + final int code; + final String message; + Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); + if (cause == null) + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = th.toString(); + } + else if (cause instanceof BadMessageException) + { + BadMessageException bme = (BadMessageException)cause; + code = bme.getCode(); + message = bme.getReason(); + } + else if (cause instanceof UnavailableException) + { + message = cause.toString(); + if (((UnavailableException)cause).isPermanent()) + code = HttpStatus.NOT_FOUND_404; + else + code = HttpStatus.SERVICE_UNAVAILABLE_503; + } else - code = HttpStatus.SERVICE_UNAVAILABLE_503; - } + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = null; + } + + request.setAttribute(ERROR_EXCEPTION, th); + request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); + sendError(code, message); + }; final AsyncContextEvent asyncEvent; final List asyncListeners; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { + if (LOG.isDebugEnabled()) + LOG.debug("thrownException " + getStatusStringLocked(), th); + // This can only be called from within the handle loop - switch (_state) + if (_state != State.HANDLING) + throw new IllegalStateException(getStatusStringLocked()); + + if (_sendError) { - case ASYNC_WAIT: - case ASYNC_WOKEN: - case IDLE: - throw new IllegalStateException(_state.toString()); - default: - break; + LOG.warn("unhandled due to prior sendError", th); + return; } // Check async state to determine type of handling - switch (_async) + switch (_lifeCycleState) { - case NOT_ASYNC: - // error not in async, will be handled by error handler in normal handler loop. - asyncEvent = null; - asyncListeners = null; - if (_sendError) - { - LOG.warn("unhandled due to prior sendError in state " + _async, cause); - return; - } - break; + case BLOCKING: + // handle the exception with a sendError + sendError.run(); + return; case DISPATCH: case COMPLETE: - case EXPIRED: - if (_state != State.DISPATCHED) + // Complete or Dispatch have been called, but the original subsequently threw an exception. + // TODO // GW I think we really should ignore, but will fall through for now. + // TODO LOG.warn("unhandled due to prior dispatch/complete", th); + // TODO return; + + case ASYNC: + if (_asyncListeners == null || _asyncListeners.isEmpty()) { - LOG.warn("unhandled in state " + _state + "/" + _async, cause); + sendError.run(); return; } - // Async life cycle method has been correctly called, but not yet acted on - // so we can fall through to same action as STARTED - - case STARTED: asyncEvent = _event; asyncEvent.addThrowable(th); - if (_asyncListeners == null || _asyncListeners.isEmpty()) - { - // error in async, but no listeners so handle it with error dispatch - if (_sendError) - { - LOG.warn("unhandled due to prior sendError in state " + _async, cause); - return; - } - asyncListeners = null; - _async = Async.ERRORED; - } - else - { - // error in async with listeners, so give them a chance to handle - asyncListeners = _asyncListeners; - _async = Async.ERRORING; - _sendError = false; - } + asyncListeners = _asyncListeners; break; default: - LOG.warn("unhandled in state " + _async, new IllegalStateException(cause)); + LOG.warn("unhandled in state " + _lifeCycleState, new IllegalStateException(th)); return; } } // If we are async and have async listeners - if (asyncEvent != null && asyncListeners != null) + // call onError + runInContext(asyncEvent, () -> { - // call onError - Runnable task = new Runnable() + for (AsyncListener listener : asyncListeners) { - @Override - public void run() + try { - for (AsyncListener listener : asyncListeners) - { - try - { - listener.onError(asyncEvent); - } - catch (Throwable x) - { - LOG.warn(x + " while invoking onError listener " + listener); - LOG.debug(x); - } - } + listener.onError(asyncEvent); } - - @Override - public String toString() + catch (Throwable x) { - return "onError"; + LOG.warn(x + " while invoking onError listener " + listener); + LOG.debug(x); } - }; - runInContext(asyncEvent, task); + } + }); + + // check the actions of the listeners + synchronized (this) + { + // if anybody has called sendError then we've handled as much as we can by calling listeners + if (_sendError) + return; - // check the actions of the listeners - try (Locker.Lock lock = _locker.lock()) + switch (_lifeCycleState) { - // if anybody has called sendError then we've handled as much as we can by calling listeners - if (_sendError) + case ASYNC: + // The listeners did not invoke API methods + // and the container must provide a default error dispatch. + sendError.run(); return; - switch (_async) - { - case ERRORING: - // The listeners did not invoke API methods - // and the container must provide a default error dispatch. - _async = Async.ERRORED; - break; - - case DISPATCH: - case COMPLETE: - case NOT_ASYNC: - // The listeners handled the exception by calling dispatch() or complete(). - return; + case DISPATCH: + case COMPLETE: + // The listeners handled the exception by calling dispatch() or complete(). + return; - default: - LOG.warn("unhandled in state " + _async, new IllegalStateException(cause)); - return; - } + default: + LOG.warn("unhandled in state " + _lifeCycleState, new IllegalStateException(th)); + return; } } - - // handle the exception with a sendError dispatch - final Request request = _channel.getRequest(); - request.setAttribute(ERROR_EXCEPTION, cause); - request.setAttribute(ERROR_EXCEPTION_TYPE, cause.getClass()); - sendError(code, message); } public void sendError(int code, String message) @@ -914,40 +922,40 @@ public void sendError(int code, String message) final Request request = _channel.getRequest(); final Response response = _channel.getResponse(); - - response.resetContent(); - response.getHttpOutput().sendErrorClose(); - if (message == null) message = HttpStatus.getMessage(code); - request.getResponse().setStatus(code); - // we are allowed to have a body, then produce the error page. - ContextHandler.Context context = request.getErrorContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); - request.setAttribute(ERROR_STATUS_CODE, code); - request.setAttribute(ERROR_MESSAGE, message); - - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { + if (_responseState != ResponseState.OPEN) + throw new IllegalStateException("Response is " + _responseState); + response.resetContent(); // will throw ISE if committed + response.getHttpOutput().sendErrorClose(); + + request.getResponse().setStatus(code); + // we are allowed to have a body, then produce the error page. + ContextHandler.Context context = request.getErrorContext(); + if (context != null) + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + if (LOG.isDebugEnabled()) LOG.debug("sendError {}", toStringLocked()); switch (_state) { - case DISPATCHED: - case COMPLETING: - case ASYNC_WOKEN: - case ASYNC_ERROR: - case ASYNC_WAIT: + case HANDLING: + case WAKING: + case WAITING: _sendError = true; if (_event != null) { Throwable cause = (Throwable) request.getAttribute(ERROR_EXCEPTION); - _event.addThrowable(cause == null ? SEND_ERROR_CAUSE : cause); + if (cause != null) + _event.addThrowable(cause); } break; @@ -964,24 +972,20 @@ protected void completed() final List aListeners; final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onComplete {}", toStringLocked()); - switch (_state) + switch (_lifeCycleState) { case COMPLETING: aListeners = _asyncListeners; event = _event; - _state = State.COMPLETED; - _async = Async.NOT_ASYNC; + _lifeCycleState = LifeCycleState.COMPLETED; break; default: - System.err.println(this.getStatusStringLocked()); - new Throwable().printStackTrace(); - System.exit(1); throw new IllegalStateException(this.getStatusStringLocked()); } } @@ -1025,14 +1029,14 @@ public String toString() protected void recycle() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("recycle {}", toStringLocked()); switch (_state) { - case DISPATCHED: + case HANDLING: throw new IllegalStateException(getStatusStringLocked()); case UPGRADED: return; @@ -1041,9 +1045,10 @@ protected void recycle() } _asyncListeners = null; _state = State.IDLE; - _async = Async.NOT_ASYNC; + _lifeCycleState = LifeCycleState.BLOCKING; + _responseState = ResponseState.OPEN; _initial = true; - _asyncRead = AsyncRead.IDLE; + _asyncReadState = AsyncReadState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -1053,7 +1058,7 @@ protected void recycle() public void upgrade() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("upgrade {}", toStringLocked()); @@ -1061,16 +1066,15 @@ public void upgrade() switch (_state) { case IDLE: - case COMPLETED: break; default: throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; _state = State.UPGRADED; - _async = Async.NOT_ASYNC; + _lifeCycleState = LifeCycleState.BLOCKING; _initial = true; - _asyncRead = AsyncRead.IDLE; + _asyncReadState = AsyncReadState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -1085,7 +1089,7 @@ protected void scheduleDispatch() protected void cancelTimeout() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1100,7 +1104,7 @@ protected void cancelTimeout(AsyncContextEvent event) public boolean isIdle() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state == State.IDLE; } @@ -1108,15 +1112,16 @@ public boolean isIdle() public boolean isExpired() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _async == Async.EXPIRED; + // TODO review + return _lifeCycleState == LifeCycleState.EXPIRE || _lifeCycleState == LifeCycleState.EXPIRING; } } public boolean isInitial() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _initial; } @@ -1124,51 +1129,43 @@ public boolean isInitial() public boolean isSuspended() { - try (Locker.Lock lock = _locker.lock()) - { - return _state == State.ASYNC_WAIT || _state == State.DISPATCHED && _async == Async.STARTED; - } - } - - boolean isCompleting() - { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.COMPLETING; + return _state == State.WAITING || _state == State.HANDLING && _lifeCycleState == LifeCycleState.ASYNC; } } boolean isCompleted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.COMPLETED; + return _lifeCycleState == LifeCycleState.COMPLETED; } } public boolean isAsyncStarted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - if (_state == State.DISPATCHED) - return _async != Async.NOT_ASYNC; - return _async == Async.STARTED || _async == Async.EXPIRING; + if (_state == State.HANDLING) + return _lifeCycleState != LifeCycleState.BLOCKING; + return _lifeCycleState == LifeCycleState.ASYNC || _lifeCycleState == LifeCycleState.EXPIRING; } } public boolean isAsyncComplete() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _async == Async.COMPLETE; + return _lifeCycleState == LifeCycleState.COMPLETE; } } public boolean isAsync() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return !_initial || _async != Async.NOT_ASYNC; + return !_initial || _lifeCycleState != LifeCycleState.BLOCKING; } } @@ -1185,7 +1182,7 @@ public HttpChannel getHttpChannel() public ContextHandler getContextHandler() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1206,7 +1203,7 @@ ContextHandler getContextHandler(AsyncContextEvent event) public ServletResponse getServletResponse() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1254,23 +1251,23 @@ public void setAttribute(String name, Object attribute) public void onReadUnready() { boolean interested = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadUnready {}", toStringLocked()); - switch (_asyncRead) + switch (_asyncReadState) { case IDLE: case READY: - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { interested = true; - _asyncRead = AsyncRead.REGISTERED; + _asyncReadState = AsyncReadState.REGISTERED; } else { - _asyncRead = AsyncRead.REGISTER; + _asyncReadState = AsyncReadState.REGISTER; } break; @@ -1297,28 +1294,28 @@ public void onReadUnready() public boolean onContentAdded() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onContentAdded {}", toStringLocked()); - switch (_asyncRead) + switch (_asyncReadState) { case IDLE: case READY: break; case PRODUCING: - _asyncRead = AsyncRead.READY; + _asyncReadState = AsyncReadState.READY; break; case REGISTER: case REGISTERED: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _asyncReadState = AsyncReadState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WAKING; } break; @@ -1340,19 +1337,19 @@ public boolean onContentAdded() public boolean onReadReady() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadReady {}", toStringLocked()); - switch (_asyncRead) + switch (_asyncReadState) { case IDLE: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _asyncReadState = AsyncReadState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WAKING; } break; @@ -1373,19 +1370,19 @@ public boolean onReadReady() public boolean onReadPossible() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadPossible {}", toStringLocked()); - switch (_asyncRead) + switch (_asyncReadState) { case REGISTERED: - _asyncRead = AsyncRead.POSSIBLE; - if (_state == State.ASYNC_WAIT) + _asyncReadState = AsyncReadState.POSSIBLE; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WAKING; } break; @@ -1405,17 +1402,17 @@ public boolean onReadPossible() public boolean onReadEof() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onEof {}", toStringLocked()); // Force read ready so onAllDataRead can be called - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _asyncReadState = AsyncReadState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WAKING; } } return woken; @@ -1425,15 +1422,15 @@ public boolean onWritePossible() { boolean wake = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { - _state = State.ASYNC_WOKEN; + _state = State.WAKING; wake = true; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index ba7040c2bd31..80c31516e00a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -278,17 +278,8 @@ else if (filled == 0) } else if (filled < 0) { - switch (_channel.getState().getState()) - { - case COMPLETING: - case COMPLETED: - case IDLE: - case ASYNC_ERROR: - getEndPoint().shutdownOutput(); - break; - default: - break; - } + if (_channel.getState().isIdle()) + getEndPoint().shutdownOutput(); break; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 762e1da1c9d5..9ab2f6173d23 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -142,18 +142,18 @@ default void resetBuffer() throws IllegalStateException private volatile Throwable _onError; /* - ACTION OPEN ASYNC READY PENDING UNREADY CLOSED - ------------------------------------------------------------------------------------------- - setWriteListener() READY->owp ise ise ise ise ise - write() OPEN ise PENDING wpe wpe eof - flush() OPEN ise PENDING wpe wpe eof - close() CLOSED CLOSED CLOSED CLOSED CLOSED CLOSED - isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true - write completed - - - ASYNC READY->owp - + ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED + -------------------------------------------------------------------------------------------------- + setWriteListener() READY->owp ise ise ise ise ise ise + write() OPEN ise PENDING wpe wpe eof eof + flush() OPEN ise PENDING wpe wpe eof eof + close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED + isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true + write completed - - - ASYNC READY->owp CLOSED - */ private enum OutputState { - OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSED + OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSING, CLOSED } private final AtomicReference _state = new AtomicReference<>(OutputState.OPEN); @@ -284,6 +284,7 @@ public void close() OutputState state = _state.get(); switch (state) { + case CLOSING: case CLOSED: { return; @@ -292,12 +293,11 @@ public void close() { // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. - // However it is desirable to allow a close at any time, specially if + // However it is desirable to allow a close at any time, specially if // complete is called. Thus we simulate a call to isReady here, assuming // that we can transition to READY. - if (!_state.compareAndSet(state, OutputState.READY))// TODO review this! Why READY? // Should it continue? - continue; - break; + _state.compareAndSet(state, OutputState.READY); + continue; } case UNREADY: case PENDING: @@ -318,7 +318,7 @@ public void close() } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, OutputState.CLOSING)) continue; // Do a normal close by writing the aggregate buffer or an empty buffer. If we are @@ -331,10 +331,6 @@ public void close() { LOG.ignore(x); // Ignore it, it's been already logged in write(). } - finally - { - releaseBuffer(); - } // Return even if an exception is thrown by write(). return; } @@ -368,6 +364,7 @@ public void closed() if (!_state.compareAndSet(state, OutputState.CLOSED)) break; + // Just make sure write and output stream really are closed try { _channel.getResponse().closeOutput(); @@ -402,6 +399,7 @@ public boolean isClosed() { switch (_state.get()) { + case CLOSING: case CLOSED: return true; default: @@ -444,15 +442,14 @@ public void flush() throws IOException new AsyncFlush().iterate(); return; - case PENDING: - return; - case UNREADY: throw new WritePendingException(); case ERROR: throw new EofException(_onError); + case PENDING: + case CLOSING: case CLOSED: return; @@ -516,6 +513,7 @@ public void write(byte[] b, int off, int len) throws IOException case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -582,9 +580,6 @@ else if (last) { write(BufferUtil.EMPTY_BUFFER, true); } - - if (last) - closed(); } public void write(ByteBuffer buffer) throws IOException @@ -620,6 +615,7 @@ public void write(ByteBuffer buffer) throws IOException case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -642,9 +638,6 @@ public void write(ByteBuffer buffer) throws IOException write(buffer, last); else if (last) write(BufferUtil.EMPTY_BUFFER, true); - - if (last) - closed(); } @Override @@ -665,11 +658,7 @@ public void write(int b) throws IOException // Check if all written or full if (complete || BufferUtil.isFull(_aggregate)) - { write(_aggregate, complete); - if (complete) - closed(); - } break; case ASYNC: @@ -702,6 +691,7 @@ public void write(int b) throws IOException case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -839,7 +829,6 @@ public void sendContent(ByteBuffer content) throws IOException _written += content.remaining(); write(content, true); - closed(); } /** @@ -1003,6 +992,7 @@ public void sendContent(HttpContent httpContent, Callback callback) callback.failed(new EofException(_onError)); return; + case CLOSING: case CLOSED: callback.failed(new EofException("Closed")); return; @@ -1139,6 +1129,7 @@ public boolean isReady() case OPEN: case READY: case ERROR: + case CLOSING: case CLOSED: return true; @@ -1172,6 +1163,7 @@ public void run() { switch (state) { + case CLOSING: case CLOSED: case ERROR: { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 9ca5aaaf14ae..15788092c66e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -27,6 +27,8 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.servlet.AsyncContext; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -41,6 +43,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ByteArrayOutputStream2; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -212,6 +216,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques case "text/*": case "*/*": { + // We can generate an acceptable contentType, but can we generate an acceptable charset? Charset charset = null; List acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); if (acceptable.isEmpty()) @@ -237,21 +242,70 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques } } + // If we have no acceptable charset, don't write an error page. if (charset == null) return; - ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); - PrintWriter writer = new PrintWriter(new OutputStreamWriter(bout, charset)); + // We can write an error page! baseRequest.setHandled(true); + + // We will write it into a byte array buffer so + // we can flush it asynchronously. + ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(bout, charset)); response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); response.setCharacterEncoding(charset.name()); handleErrorPage(request, writer, code, message); writer.flush(); ByteBuffer content = bout.size() == 0 ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(bout.getBuf(), 0, bout.size()); - // TODO use the non blocking version - baseRequest.getHttpChannel().sendResponse(null, content, true); - baseRequest.getResponse().getHttpOutput().closed(); + // Can we write asynchronously + if (!request.isAsyncSupported()) + { + // TODO write a test for this path + // No - have to do a blocking write + baseRequest.getHttpChannel().sendResponse(null, content, true); + return; + } + + // As most errors are small, asynchronous writes will frequently succeed withing the sendResponse + // call, so we write the error response asynchronously before calling startAsync + final FuturePromise async = new FuturePromise<>(); + final AtomicBoolean written = new AtomicBoolean(); + baseRequest.getHttpChannel().sendResponse(null, content, true, Callback.from(() -> + { + // if we wrote within the sendResponse call, we will win this race and have nothing more to do + if (!written.compareAndSet(false, true)) + { + // TODO write a test for this path + // we lost the written race, so we have to wait for the ErrorHandler to startAsync (or fail) + // and then call complete + try + { + async.get().complete(); + } + catch (Throwable e) + { + LOG.ignore(e); + } + } + })); + + // We will only win this race if the sendResponse above has not completed/ + if (written.compareAndSet(false, true)) + { + try + { + // so we must startAsync and pass that to the callback above. + async.succeeded(request.startAsync()); + // request handling will now proceed as if we did an error page dispatch and it called startAsync + } + catch (Throwable t) + { + // or fail in the attempt + async.failed(t); + } + } } default: diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java index b88126734792..13be338ff7fe 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToAsyncCommitTest.java @@ -970,11 +970,11 @@ private void runAsyncInAsyncWait(Request request, Runnable task) { switch (request.getHttpChannelState().getState()) { - case ASYNC_WAIT: + case WAITING: task.run(); return; - case DISPATCHED: + case HANDLING: Thread.sleep(100); continue; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java index a06fc72e6fce..c2f09727ebe6 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/LocalAsyncContextTest.java @@ -32,6 +32,8 @@ import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,6 +44,7 @@ public class LocalAsyncContextTest { + public static final Logger LOG = Log.getLogger(LocalAsyncContextTest.class); protected Server _server; protected SuspendHandler _handler; protected Connector _connector; @@ -232,6 +235,7 @@ protected void check(String response, String... content) private synchronized String process(String content) throws Exception { + LOG.debug("TEST process: {}", content); reset(); String request = "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + @@ -305,6 +309,7 @@ public void setCompleteAfter2(long completeAfter2) @Override public void handle(String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { + LOG.debug("handle {} {}", baseRequest.getDispatcherType(), baseRequest); if (DispatcherType.REQUEST.equals(baseRequest.getDispatcherType())) { if (_read > 0) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index 6ff9f9b3ddab..e318b2ee788f 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -878,7 +878,7 @@ public void testZeroContent() throws Exception assertTrue(!response.isCommitted()); assertTrue(!writer.checkError()); writer.print(""); - assertTrue(!writer.checkError()); + // assertTrue(!writer.checkError()); // TODO check if this is correct? checkout does an open check and the print above closes assertTrue(response.isCommitted()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java index 763d19eb5b1e..2230817880c7 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java @@ -37,6 +37,7 @@ import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.RequestLog; @@ -391,7 +392,7 @@ public static Stream data() data.add(new Object[]{logType, new IOExceptionPartialHandler(), "/ioex", "\"GET /ioex HTTP/1.0\" 200"}); data.add(new Object[]{logType, new RuntimeExceptionHandler(), "/rtex", "\"GET /rtex HTTP/1.0\" 500"}); data.add(new Object[]{logType, new BadMessageHandler(), "/bad", "\"GET /bad HTTP/1.0\" 499"}); - data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 488"}); + data.add(new Object[]{logType, new AbortHandler(), "/bad", "\"GET /bad HTTP/1.0\" 500"}); data.add(new Object[]{logType, new AbortPartialHandler(), "/bad", "\"GET /bad HTTP/1.0\" 200"}); } @@ -518,7 +519,9 @@ protected void doNonErrorHandle(String target, Request baseRequest, HttpServletR startServer(); makeRequest(requestPath); - expectedLogEntry = "\"GET " + requestPath + " HTTP/1.0\" 200"; + // If we abort, we can't write a 200 error page + if (!(testHandler instanceof AbortHandler)) + expectedLogEntry = expectedLogEntry.replaceFirst(" [1-9][0-9][0-9]", " 200"); assertRequestLog(expectedLogEntry, _log); } @@ -578,6 +581,10 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { try { + while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING) + { + Thread.yield(); + } baseRequest.setHandled(false); testHandler.handle(target, baseRequest, request, response); if (!baseRequest.isHandled()) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index e733877da95c..89f607e6c7d2 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -447,7 +447,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t hold.countDown(); // Wait until request async waiting - while (Request.getBaseRequest(request).getHttpChannelState().getState() != HttpChannelState.State.ASYNC_WAIT) + while (Request.getBaseRequest(request).getHttpChannelState().getState() != HttpChannelState.State.WAITING) { try { From ed3ba54c537eb0af4804cbc43396f7d8d3c35c0f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 29 Jul 2019 23:45:50 +1000 Subject: [PATCH 38/57] Issue #3806 async sendError more WIP on fully async error handling. Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 9 +- .../jetty/server/HttpChannelState.java | 92 ++++++++-------- .../jetty/server/HttpInputAsyncStateTest.java | 4 + .../jetty/servlet/AsyncListenerTest.java | 4 +- .../jetty/servlet/AsyncServletIOTest.java | 8 +- .../eclipse/jetty/servlet/ErrorPageTest.java | 104 +++++++++++++----- 6 files changed, 137 insertions(+), 84 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index f5a6b130da28..f7fa4d5c4efa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -505,6 +505,11 @@ public boolean handle() throw _state.getAsyncContextEvent().getThrowable(); } + case READ_REGISTER: + { + onAsyncWaitForContent(); + } + case READ_PRODUCE: { _request.getHttpInput().asyncReadProduce(); @@ -670,11 +675,11 @@ public boolean isExpecting102Processing() public String toString() { long timeStamp = _request.getTimeStamp(); - return String.format("%s@%x{r=%s,c=%b,c=%b/%b,a=%s,uri=%s,age=%d}", + return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}", getClass().getSimpleName(), hashCode(), + _state, _requests, - _state.isResponseCommitted(), isRequestCompleted(), isResponseCompleted(), _state.getState(), diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 7010e988a7a8..521e5c550ac8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -109,6 +109,7 @@ public enum Action ASYNC_ERROR, // handle an async error ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback + READ_REGISTER, // Register for fill interest READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback COMPLETE, // Complete the response @@ -226,14 +227,15 @@ public String toStringLocked() private String getStatusStringLocked() { - return String.format("s=%s lc=%s rs=%s ars=%s awp=%b se=%b i=%b", + return String.format("s=%s lc=%s rs=%s ars=%s awp=%b se=%b i=%b al=%d", _state, _lifeCycleState, _responseState, _asyncReadState, _asyncWritePossible, _sendError, - _initial); + _initial, + _asyncListeners == null ? 0 : _asyncListeners.size()); } public String getStatusString() @@ -388,52 +390,47 @@ protected Action unhandle() synchronized (this) { - try - { - if (LOG.isDebugEnabled()) - LOG.debug("unhandle {}", toStringLocked()); + if (LOG.isDebugEnabled()) + LOG.debug("unhandle {}", toStringLocked()); - switch (_state) - { - case HANDLING: - break; + switch (_state) + { + case HANDLING: + break; - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } + default: + throw new IllegalStateException(this.getStatusStringLocked()); + } - _initial = false; + _initial = false; - Action action = nextAction(false); - if (LOG.isDebugEnabled()) - LOG.debug("nextAction(false) {} {}", action, toStringLocked()); - return action; - } - finally - { - if (_state == State.WAITING) - { - switch(_asyncReadState) - { - case REGISTER: - case PRODUCING: - _channel.onAsyncWaitForContent(); - default: - break; - } - } - } + Action action = nextAction(false); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(false) {} {}", action, toStringLocked()); + return action; } } private Action nextAction(boolean handling) { + _state = State.HANDLING; + if (_sendError) { - _state = State.HANDLING; - _lifeCycleState = LifeCycleState.BLOCKING; - _sendError = false; - return Action.ERROR_DISPATCH; + switch (_lifeCycleState) + { + case BLOCKING: + case ASYNC: + case COMPLETE: + case DISPATCH: + case COMPLETING: + _lifeCycleState = LifeCycleState.BLOCKING; + _sendError = false; + return Action.ERROR_DISPATCH; + + default: + break; + } } switch (_lifeCycleState) @@ -441,7 +438,6 @@ private Action nextAction(boolean handling) case BLOCKING: if (handling) throw new IllegalStateException(getStatusStringLocked()); - _state = State.HANDLING; _lifeCycleState = LifeCycleState.COMPLETING; return Action.COMPLETE; @@ -449,15 +445,19 @@ private Action nextAction(boolean handling) switch (_asyncReadState) { case POSSIBLE: - _state = State.HANDLING; _asyncReadState = AsyncReadState.PRODUCING; return Action.READ_PRODUCE; case READY: - _state = State.HANDLING; _asyncReadState = AsyncReadState.IDLE; return Action.READ_CALLBACK; case REGISTER: case PRODUCING: + if (!handling) + { + _asyncReadState = AsyncReadState.REGISTERED; + return Action.READ_REGISTER; + } + break; case IDLE: case REGISTERED: break; @@ -467,13 +467,12 @@ private Action nextAction(boolean handling) if (_asyncWritePossible) { - _state = State.HANDLING; _asyncWritePossible = false; return Action.WRITE_CALLBACK; } if (handling) - throw new IllegalStateException(getStatusStringLocked()); + return Action.NOOP; Scheduler scheduler = _channel.getScheduler(); if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) @@ -482,12 +481,10 @@ private Action nextAction(boolean handling) return Action.WAIT; case DISPATCH: - _state = State.HANDLING; _lifeCycleState = LifeCycleState.BLOCKING; return Action.ASYNC_DISPATCH; case EXPIRE: - _state = State.HANDLING; _lifeCycleState = LifeCycleState.EXPIRING; return Action.ASYNC_TIMEOUT; @@ -497,7 +494,6 @@ private Action nextAction(boolean handling) // We must have already called onTimeout and nothing changed, // so we will do a normal error dispatch - _state = State.HANDLING; _lifeCycleState = LifeCycleState.BLOCKING; final Request request = _channel.getRequest(); @@ -510,16 +506,12 @@ private Action nextAction(boolean handling) return Action.ERROR_DISPATCH; case COMPLETE: - _state = State.HANDLING; _lifeCycleState = LifeCycleState.COMPLETING; return Action.COMPLETE; case COMPLETING: if (handling) - { - _state = State.HANDLING; return Action.COMPLETE; - } _state = State.WAITING; return Action.WAIT; @@ -853,6 +845,7 @@ else if (cause instanceof UnavailableException) if (_asyncListeners == null || _asyncListeners.isEmpty()) { sendError.run(); + _lifeCycleState = LifeCycleState.BLOCKING; return; } asyncEvent = _event; @@ -897,6 +890,7 @@ else if (cause instanceof UnavailableException) // The listeners did not invoke API methods // and the container must provide a default error dispatch. sendError.run(); + _lifeCycleState = LifeCycleState.BLOCKING; return; case DISPATCH: diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java index 9c667a9b6c8e..fcabf1acfe9a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java @@ -190,6 +190,10 @@ private void handle(Runnable run) __history.add("COMPLETE"); break; + case READ_REGISTER: + _state.getHttpChannel().onAsyncWaitForContent(); + break; + default: fail("Bad Action: " + action); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java index 4efbb1d2b075..6d6bb5824ed4 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java @@ -194,7 +194,7 @@ private void test_StartAsync_Throw_OnError(IOConsumer consumer) thro protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); + asyncContext.setTimeout(10000); asyncContext.addListener(new AsyncListenerAdapter() { @Override @@ -389,7 +389,7 @@ public void test_StartAsync_OnComplete_Throw() throws Exception protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); + asyncContext.setTimeout(10000); asyncContext.addListener(new AsyncListenerAdapter() { @Override diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java index 3ea4c8044372..b80394f61f66 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java @@ -52,6 +52,7 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -305,6 +306,7 @@ public synchronized List process(byte[] content, int... writes) throws E request.append(s).append("w=").append(w); s = '&'; } + LOG.debug("process {} {}", request.toString(), BufferUtil.toDetailString(BufferUtil.toBuffer(content))); request.append(" HTTP/1.1\r\n") .append("Host: localhost\r\n") @@ -816,13 +818,15 @@ public void testStolenAsyncRead() throws Exception // wait until server is ready _servletStolenAsyncRead.ready.await(); final CountDownLatch wait = new CountDownLatch(1); - + final CountDownLatch held = new CountDownLatch(1); // Stop any dispatches until we want them + UnaryOperator old = _wQTP.wrapper.getAndSet(r -> () -> { try { + held.countDown(); wait.await(); r.run(); } @@ -836,7 +840,9 @@ public void testStolenAsyncRead() throws Exception // We are an unrelated thread, let's mess with the input stream ServletInputStream sin = _servletStolenAsyncRead.listener.in; sin.setReadListener(_servletStolenAsyncRead.listener); + // thread should be dispatched to handle, but held by our wQTP wait. + assertTrue(held.await(10, TimeUnit.SECONDS)); // Let's steal our read assertTrue(sin.isReady()); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 89f607e6c7d2..61d2cd2d1df4 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -321,11 +321,11 @@ public void testBadMessage() throws Exception } @Test - public void testAsyncErrorPage0() throws Exception + public void testAsyncErrorPageDSC() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) { - String response = _connector.getResponse("GET /async/info HTTP/1.0\r\n\r\n"); + String response = _connector.getResponse("GET /async/info?mode=DSC HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); assertThat(response, Matchers.containsString("ERROR_CODE: 599")); @@ -338,11 +338,28 @@ public void testAsyncErrorPage0() throws Exception } @Test - public void testAsyncErrorPage1() throws Exception + public void testAsyncErrorPageSDC() throws Exception { try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) { - String response = _connector.getResponse("GET /async/info?latecomplete=true HTTP/1.0\r\n\r\n"); + String response = _connector.getResponse("GET /async/info?mode=SDC HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); + assertThat(response, Matchers.containsString("ERROR_CODE: 599")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: null")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AsyncSendErrorServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /async/info")); + assertTrue(__asyncSendErrorCompleted.await(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testAsyncErrorPageSCD() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /async/info?mode=SCD HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 599 599")); assertThat(response, Matchers.containsString("ERROR_PAGE: /599")); assertThat(response, Matchers.containsString("ERROR_CODE: 599")); @@ -433,47 +450,74 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t try { final CountDownLatch hold = new CountDownLatch(1); + final String mode = request.getParameter("mode"); + switch(mode) + { + case "DSC": + case "SDC": + case "SCD": + break; + default: + throw new IllegalStateException(mode); + } + final boolean lateComplete = "true".equals(request.getParameter("latecomplete")); AsyncContext async = request.startAsync(); async.start(() -> { try { - response.sendError(599); - - if (lateComplete) + switch(mode) { - // Complete after original servlet - hold.countDown(); - - // Wait until request async waiting - while (Request.getBaseRequest(request).getHttpChannelState().getState() != HttpChannelState.State.WAITING) - { - try - { - Thread.sleep(10); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - } - async.complete(); - __asyncSendErrorCompleted.countDown(); + case "SDC": + response.sendError(599); + break; + case "SCD": + response.sendError(599); + async.complete(); + break; + default: + break; } - else + + // Complete after original servlet + hold.countDown(); + + // Wait until request async waiting + while (Request.getBaseRequest(request).getHttpChannelState().getState() == HttpChannelState.State.HANDLING) { - // Complete before original servlet try { - async.complete(); - __asyncSendErrorCompleted.countDown(); + Thread.sleep(10); } - finally + catch (InterruptedException e) { - hold.countDown(); + e.printStackTrace(); } } + try + { + switch (mode) + { + case "DSC": + response.sendError(599); + async.complete(); + break; + case "SDC": + async.complete(); + break; + default: + break; + } + } + catch(IllegalStateException e) + { + Log.getLog().ignore(e); + } + finally + { + __asyncSendErrorCompleted.countDown(); + } } catch (IOException e) { From b715f2d25439099a35df775605bf197e1881bfc8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Tue, 30 Jul 2019 12:47:30 +1000 Subject: [PATCH 39/57] Issue #3806 async sendError sendError and completion are not both non-blocking, without using a startAsync operation. However we are lacking unit tests that actually exercise those code paths. Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 43 ++++++---- .../jetty/server/HttpChannelState.java | 84 ++++++++++++------- .../org/eclipse/jetty/server/HttpOutput.java | 33 +++++++- .../jetty/server/handler/ErrorHandler.java | 55 +----------- 4 files changed, 115 insertions(+), 100 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index f7fa4d5c4efa..cb4d89117d71 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -25,11 +25,13 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; @@ -52,6 +54,7 @@ import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -421,6 +424,11 @@ public boolean handle() { try { + // Get ready to send an error response + _request.setHandled(false); + _response.resetContent(); + _response.getHttpOutput().reopen(); + // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); @@ -428,9 +436,6 @@ public boolean handle() _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); _response.setStatus(code); - _request.setHandled(false); - _response.getHttpOutput().reopen(); - ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); @@ -439,8 +444,7 @@ public boolean handle() errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - _request.setHandled(true); - minimalErrorResponse(code); + sendCompleteResponse(null); break; } @@ -488,9 +492,12 @@ public boolean handle() Throwable cause = unwrap(failure, BadMessageException.class); int code = cause instanceof BadMessageException ? ((BadMessageException)cause).getCode() : 500; + _response.setStatus(code); - if (!_state.isResponseCommitted()) - minimalErrorResponse(code); + if (_state.isResponseCommitted()) + abort(x); + else + sendCompleteResponse(null); } finally { @@ -555,8 +562,13 @@ public boolean handle() break; } } - _response.closeOutput(); // TODO make this non blocking! - _state.completed(); + + // Set a close callback on the HttpOutput to make it an async callback + _response.getHttpOutput().setClosedCallback(Callback.from(_state::completed)); // TODO test this actually works asynchronously + _response.closeOutput(); + // ensure the callback actually got called + if (_response.getHttpOutput().getClosedCallback() != null) + _state.completed(); break; } @@ -644,16 +656,19 @@ protected Throwable unwrap(Throwable failure, Class... targets) return null; } - private void minimalErrorResponse(int code) + public void sendCompleteResponse(ByteBuffer content) { try { - _response.resetContent(); - _response.setStatus(code); _request.setHandled(true); + _state.completing(); - // TODO use the non blocking version - sendResponse(null, null, true); + final FuturePromise async = new FuturePromise<>(); + final AtomicBoolean written = new AtomicBoolean(); + sendResponse(null, content, true, Callback.from(() -> + { + _state.completed(); + })); } catch (Throwable x) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 521e5c550ac8..5e818ce42a72 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -413,6 +413,7 @@ protected Action unhandle() private Action nextAction(boolean handling) { + // Assume we can keep going, but exceptions are below _state = State.HANDLING; if (_sendError) @@ -510,15 +511,11 @@ private Action nextAction(boolean handling) return Action.COMPLETE; case COMPLETING: - if (handling) - return Action.COMPLETE; - _state = State.WAITING; return Action.WAIT; case COMPLETED: _state = State.IDLE; - _lifeCycleState = LifeCycleState.COMPLETED; return Action.TERMINATED; default: @@ -961,22 +958,50 @@ public void sendError(int code, String message) } } + protected void completing() + { + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completing {}", toStringLocked()); + + switch (_lifeCycleState) + { + case COMPLETED: + throw new IllegalStateException(getStatusStringLocked()); + default: + _lifeCycleState = LifeCycleState.COMPLETING; + } + } + } + protected void completed() { final List aListeners; final AsyncContextEvent event; + boolean handle = false; synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("onComplete {}", toStringLocked()); + LOG.debug("completed {}", toStringLocked()); switch (_lifeCycleState) { case COMPLETING: - aListeners = _asyncListeners; - event = _event; - _lifeCycleState = LifeCycleState.COMPLETED; + if (_event == null) + { + _lifeCycleState = LifeCycleState.COMPLETED; + aListeners = null; + event = null; + if (_state == State.WAITING) + handle = true; + } + else + { + aListeners = _asyncListeners; + event = _event; + } break; default: @@ -986,38 +1011,37 @@ protected void completed() if (event != null) { + cancelTimeout(event); if (aListeners != null) { - Runnable callback = new Runnable() + runInContext(event, () -> { - @Override - public void run() + for (AsyncListener listener : aListeners) { - for (AsyncListener listener : aListeners) + try { - try - { - listener.onComplete(event); - } - catch (Throwable e) - { - LOG.warn(e + " while invoking onComplete listener " + listener); - LOG.debug(e); - } + listener.onComplete(event); + } + catch (Throwable e) + { + LOG.warn(e + " while invoking onComplete listener " + listener); + LOG.debug(e); } } - - @Override - public String toString() - { - return "onComplete"; - } - }; - - runInContext(event, callback); + }); } event.completed(); + + synchronized (this) + { + _lifeCycleState = LifeCycleState.COMPLETED; + if (_state == State.WAITING) + handle = true; + } } + + if (handle) + _channel.handle(); } protected void recycle() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 9ab2f6173d23..8a27b21ed1c4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -64,6 +64,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + /** * The HttpOutput.Interceptor is a single intercept point for all * output written to the HttpOutput: via writer; via output stream; @@ -140,6 +141,7 @@ default void resetBuffer() throws IllegalStateException private int _commitSize; private WriteListener _writeListener; private volatile Throwable _onError; + private Callback _closeCallback; /* ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED @@ -276,9 +278,23 @@ public void sendErrorClose() } } + + public void setClosedCallback(Callback closeCallback) + { + _closeCallback = closeCallback; + } + + public Callback getClosedCallback() + { + return _closeCallback; + } + @Override public void close() { + Callback closeCallback = _closeCallback == null ? Callback.NOOP : _closeCallback; + _closeCallback = null; + while (true) { OutputState state = _state.get(); @@ -287,6 +303,7 @@ public void close() case CLOSING: case CLOSED: { + closeCallback.succeeded(); return; } case ASYNC: @@ -314,6 +331,7 @@ public void close() LOG.warn(ex.toString()); LOG.debug(ex); abort(ex); + closeCallback.failed(ex); return; } default: @@ -325,13 +343,23 @@ public void close() // not including, then indicate this is the last write. try { - write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding()); + ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER; + if (closeCallback == Callback.NOOP) + { + // Do a blocking close + write(content, !_channel.getResponse().isIncluding()); + closeCallback.succeeded(); + } + else + { + write(content, !_channel.getResponse().isIncluding(), closeCallback); + } } catch (IOException x) { LOG.ignore(x); // Ignore it, it's been already logged in write(). + closeCallback.failed(x); } - // Return even if an exception is thrown by write(). return; } } @@ -1092,6 +1120,7 @@ public void recycle() _onError = null; _firstByteTimeStamp = -1; _flushed = 0; + _closeCallback = null; reopen(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 15788092c66e..dd8e172a5e84 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -27,8 +27,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.servlet.AsyncContext; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -43,8 +41,6 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ByteArrayOutputStream2; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -246,9 +242,6 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques if (charset == null) return; - // We can write an error page! - baseRequest.setHandled(true); - // We will write it into a byte array buffer so // we can flush it asynchronously. ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); @@ -259,53 +252,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques writer.flush(); ByteBuffer content = bout.size() == 0 ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(bout.getBuf(), 0, bout.size()); - // Can we write asynchronously - if (!request.isAsyncSupported()) - { - // TODO write a test for this path - // No - have to do a blocking write - baseRequest.getHttpChannel().sendResponse(null, content, true); - return; - } - - // As most errors are small, asynchronous writes will frequently succeed withing the sendResponse - // call, so we write the error response asynchronously before calling startAsync - final FuturePromise async = new FuturePromise<>(); - final AtomicBoolean written = new AtomicBoolean(); - baseRequest.getHttpChannel().sendResponse(null, content, true, Callback.from(() -> - { - // if we wrote within the sendResponse call, we will win this race and have nothing more to do - if (!written.compareAndSet(false, true)) - { - // TODO write a test for this path - // we lost the written race, so we have to wait for the ErrorHandler to startAsync (or fail) - // and then call complete - try - { - async.get().complete(); - } - catch (Throwable e) - { - LOG.ignore(e); - } - } - })); - - // We will only win this race if the sendResponse above has not completed/ - if (written.compareAndSet(false, true)) - { - try - { - // so we must startAsync and pass that to the callback above. - async.succeeded(request.startAsync()); - // request handling will now proceed as if we did an error page dispatch and it called startAsync - } - catch (Throwable t) - { - // or fail in the attempt - async.failed(t); - } - } + baseRequest.getHttpChannel().sendCompleteResponse(content); } default: From 1630ef2a0c82214c254ce16ff43e35d0ef76b93f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 31 Jul 2019 16:24:27 +1000 Subject: [PATCH 40/57] Simplified name of states Added test for async completion Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 9 +- .../jetty/server/HttpChannelState.java | 299 +++++++++--------- .../org/eclipse/jetty/server/HttpOutput.java | 99 +++--- .../jetty/server/handler/ErrorHandler.java | 2 +- .../jetty/server/AsyncCompletionTest.java | 220 +++++++++++++ .../jetty/server/HttpServerTestFixture.java | 28 +- .../eclipse/jetty/server/ResponseTest.java | 2 +- .../org/eclipse/jetty/util/BufferUtil.java | 12 +- 8 files changed, 462 insertions(+), 209 deletions(-) create mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index cb4d89117d71..5671b0f6d85c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -420,7 +420,7 @@ public boolean handle() _state.onTimeout(); break; - case ERROR_DISPATCH: + case SEND_ERROR: { try { @@ -515,6 +515,7 @@ public boolean handle() case READ_REGISTER: { onAsyncWaitForContent(); + break; } case READ_PRODUCE: @@ -564,11 +565,13 @@ public boolean handle() } // Set a close callback on the HttpOutput to make it an async callback - _response.getHttpOutput().setClosedCallback(Callback.from(_state::completed)); // TODO test this actually works asynchronously + _response.getHttpOutput().setClosedCallback(Callback.from(_state::completed)); _response.closeOutput(); // ensure the callback actually got called if (_response.getHttpOutput().getClosedCallback() != null) - _state.completed(); + _response.getHttpOutput().getClosedCallback().succeeded(); + + // TODO we could do an asynchronous consumeAll in the callback break; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 5e818ce42a72..af6f54d7e917 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -57,39 +57,45 @@ public class HttpChannelState */ public enum State { - IDLE, // Idle request - HANDLING, // Request dispatched to filter/servlet or Async IO callback - WAITING, // Suspended and waiting - WAKING, // Dispatch to handle from ASYNC_WAIT - UPGRADED // Request upgraded the connection + IDLE, // Idle request + HANDLING, // Request dispatched to filter/servlet or Async IO callback + WAITING, // Suspended and waiting + WOKEN, // Dispatch to handle from ASYNC_WAIT + UPGRADED // Request upgraded the connection } /** * The state of the servlet async API. */ - private enum LifeCycleState + private enum RequestState { - BLOCKING, - ASYNC, // AsyncContext.startAsync() has been called - DISPATCH, // AsyncContext.dispatch() has been called - EXPIRE, // AsyncContext timeout has happened - EXPIRING, // AsyncListeners are being called - COMPLETE, // AsyncContext.complete() has been called - COMPLETING, // Response is completable - COMPLETED // Response is completed + BLOCKING, // Blocking request dispatched + ASYNC, // AsyncContext.startAsync() has been called + DISPATCH, // AsyncContext.dispatch() has been called + EXPIRE, // AsyncContext timeout has happened + EXPIRING, // AsyncListeners are being called + COMPLETE, // AsyncContext.complete() has been called + COMPLETING, // Request is being closed (maybe asynchronously) + COMPLETED // Response is completed } - private enum AsyncReadState + /** + * @see HttpInput.State + */ + private enum InputState { - IDLE, // No isReady; No data - REGISTER, // isReady()==false handling; No data - REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async read callback called (http/1 only) - PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) - READY // isReady() was false, onContentAdded has been called + IDLE, // No isReady; No data + REGISTER, // isReady()==false handling; No data + REGISTERED, // isReady()==false !handling; No data + POSSIBLE, // isReady()==false async read callback called (http/1 only) + PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) + READY // isReady() was false, onContentAdded has been called } - private enum ResponseState + /** + * @see HttpOutput.State + */ + private enum OutputState { OPEN, COMMITTED, @@ -105,14 +111,14 @@ public enum Action NOOP, // No action DISPATCH, // handle a normal request dispatch ASYNC_DISPATCH, // handle an async request dispatch - ERROR_DISPATCH, // handle a normal error + SEND_ERROR, // Generate an error page or error dispatch ASYNC_ERROR, // handle an async error ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback - READ_REGISTER, // Register for fill interest + READ_REGISTER, // Register for fill interest READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback - COMPLETE, // Complete the response + COMPLETE, // Complete the response by closing output TERMINATED, // No further actions WAIT, // Wait for further events } @@ -120,23 +126,19 @@ public enum Action private final Locker _locker = new Locker(); private final HttpChannel _channel; private List _asyncListeners; - private State _state; - private LifeCycleState _lifeCycleState; - private ResponseState _responseState; - private AsyncReadState _asyncReadState = AsyncReadState.IDLE; - private boolean _initial; + private State _state = HttpChannelState.State.IDLE; + private RequestState _requestState = RequestState.BLOCKING; + private OutputState _outputState = OutputState.OPEN; + private InputState _inputState = InputState.IDLE; + private boolean _initial = true; + private boolean _sendError; private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; - private boolean _sendError; protected HttpChannelState(HttpChannel channel) { _channel = channel; - _state = State.IDLE; - _lifeCycleState = LifeCycleState.BLOCKING; - _responseState = ResponseState.OPEN; - _initial = true; } public State getState() @@ -229,9 +231,9 @@ private String getStatusStringLocked() { return String.format("s=%s lc=%s rs=%s ars=%s awp=%b se=%b i=%b al=%d", _state, - _lifeCycleState, - _responseState, - _asyncReadState, + _requestState, + _outputState, + _inputState, _asyncWritePossible, _sendError, _initial, @@ -250,10 +252,10 @@ public boolean commitResponse() { synchronized (this) { - switch(_responseState) + switch (_outputState) { case OPEN: - _responseState = ResponseState.COMMITTED; + _outputState = OutputState.COMMITTED; return true; default: @@ -266,10 +268,10 @@ public boolean partialResponse() { synchronized (this) { - switch(_responseState) + switch (_outputState) { case COMMITTED: - _responseState = ResponseState.OPEN; + _outputState = OutputState.OPEN; return true; default: @@ -282,11 +284,11 @@ public boolean completeResponse() { synchronized (this) { - switch(_responseState) + switch (_outputState) { case OPEN: case COMMITTED: - _responseState = ResponseState.COMPLETED; + _outputState = OutputState.COMPLETED; return true; default: @@ -299,7 +301,7 @@ public boolean isResponseCommitted() { synchronized (this) { - switch (_responseState) + switch (_outputState) { case OPEN: return false; @@ -313,7 +315,7 @@ public boolean isResponseCompleted() { synchronized (this) { - return _responseState == ResponseState.COMPLETED; + return _outputState == OutputState.COMPLETED; } } @@ -321,19 +323,18 @@ public boolean abortResponse() { synchronized (this) { - switch(_responseState) + switch (_outputState) { case ABORTED: return false; + case OPEN: - // No response has been committed - // TODO we need a better way to signal to the request log that an abort was done _channel.getResponse().setStatus(500); - _responseState = ResponseState.ABORTED; + _outputState = OutputState.ABORTED; return true; default: - _responseState = ResponseState.ABORTED; + _outputState = OutputState.ABORTED; return true; } } @@ -352,13 +353,13 @@ public Action handling() switch (_state) { case IDLE: - if (_lifeCycleState != LifeCycleState.BLOCKING) + if (_requestState != RequestState.BLOCKING) throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = State.HANDLING; + _state = HttpChannelState.State.HANDLING; return Action.DISPATCH; - case WAKING: + case WOKEN: if (_event != null && _event.getThrowable() != null && !_sendError) return Action.ASYNC_ERROR; @@ -414,48 +415,48 @@ protected Action unhandle() private Action nextAction(boolean handling) { // Assume we can keep going, but exceptions are below - _state = State.HANDLING; + _state = HttpChannelState.State.HANDLING; if (_sendError) { - switch (_lifeCycleState) + switch (_requestState) { case BLOCKING: case ASYNC: case COMPLETE: case DISPATCH: case COMPLETING: - _lifeCycleState = LifeCycleState.BLOCKING; + _requestState = RequestState.BLOCKING; _sendError = false; - return Action.ERROR_DISPATCH; + return Action.SEND_ERROR; default: break; } } - switch (_lifeCycleState) + switch (_requestState) { case BLOCKING: if (handling) throw new IllegalStateException(getStatusStringLocked()); - _lifeCycleState = LifeCycleState.COMPLETING; + _requestState = RequestState.COMPLETING; return Action.COMPLETE; case ASYNC: - switch (_asyncReadState) + switch (_inputState) { case POSSIBLE: - _asyncReadState = AsyncReadState.PRODUCING; + _inputState = InputState.PRODUCING; return Action.READ_PRODUCE; case READY: - _asyncReadState = AsyncReadState.IDLE; + _inputState = InputState.IDLE; return Action.READ_CALLBACK; case REGISTER: case PRODUCING: if (!handling) { - _asyncReadState = AsyncReadState.REGISTERED; + _inputState = InputState.REGISTERED; return Action.READ_REGISTER; } break; @@ -478,15 +479,15 @@ private Action nextAction(boolean handling) Scheduler scheduler = _channel.getScheduler(); if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - _state = State.WAITING; + _state = HttpChannelState.State.WAITING; return Action.WAIT; case DISPATCH: - _lifeCycleState = LifeCycleState.BLOCKING; + _requestState = RequestState.BLOCKING; return Action.ASYNC_DISPATCH; case EXPIRE: - _lifeCycleState = LifeCycleState.EXPIRING; + _requestState = RequestState.EXPIRING; return Action.ASYNC_TIMEOUT; case EXPIRING: @@ -495,7 +496,7 @@ private Action nextAction(boolean handling) // We must have already called onTimeout and nothing changed, // so we will do a normal error dispatch - _lifeCycleState = LifeCycleState.BLOCKING; + _requestState = RequestState.BLOCKING; final Request request = _channel.getRequest(); ContextHandler.Context context = _event.getContext(); @@ -504,18 +505,18 @@ private Action nextAction(boolean handling) request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); request.setAttribute(ERROR_STATUS_CODE, 500); request.setAttribute(ERROR_MESSAGE, "AsyncContext timeout"); - return Action.ERROR_DISPATCH; + return Action.SEND_ERROR; case COMPLETE: - _lifeCycleState = LifeCycleState.COMPLETING; + _requestState = RequestState.COMPLETING; return Action.COMPLETE; case COMPLETING: - _state = State.WAITING; + _state = HttpChannelState.State.WAITING; return Action.WAIT; case COMPLETED: - _state = State.IDLE; + _state = HttpChannelState.State.IDLE; return Action.TERMINATED; default: @@ -523,7 +524,6 @@ private Action nextAction(boolean handling) } } - public void startAsync(AsyncContextEvent event) { final List lastAsyncListeners; @@ -532,10 +532,10 @@ public void startAsync(AsyncContextEvent event) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != State.HANDLING || _lifeCycleState != LifeCycleState.BLOCKING) + if (_state != HttpChannelState.State.HANDLING || _requestState != RequestState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); - _lifeCycleState = LifeCycleState.ASYNC; + _requestState = RequestState.ASYNC; _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; @@ -586,7 +586,7 @@ public void dispatch(ServletContext context, String path) boolean started = false; event = _event; - switch (_lifeCycleState) + switch (_requestState) { case ASYNC: started = true; @@ -596,7 +596,7 @@ public void dispatch(ServletContext context, String path) default: throw new IllegalStateException(this.getStatusStringLocked()); } - _lifeCycleState = LifeCycleState.DISPATCH; + _requestState = RequestState.DISPATCH; if (context != null) _event.setDispatchContext(context); @@ -608,10 +608,10 @@ public void dispatch(ServletContext context, String path) switch (_state) { case HANDLING: - case WAKING: + case WOKEN: break; case WAITING: - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; dispatch = true; break; default: @@ -634,13 +634,13 @@ protected void timeout() if (LOG.isDebugEnabled()) LOG.debug("Timeout {}", toStringLocked()); - if (_lifeCycleState != LifeCycleState.ASYNC) + if (_requestState != RequestState.ASYNC) return; - _lifeCycleState = LifeCycleState.EXPIRE; + _requestState = RequestState.EXPIRE; - if (_state == State.WAITING) + if (_state == HttpChannelState.State.WAITING) { - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; dispatch = true; } } @@ -661,7 +661,7 @@ protected void onTimeout() { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - if (_lifeCycleState != LifeCycleState.EXPIRING || _state != State.HANDLING) + if (_requestState != RequestState.EXPIRING || _state != HttpChannelState.State.HANDLING) throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; @@ -709,11 +709,11 @@ public void complete() LOG.debug("complete {}", toStringLocked()); event = _event; - switch (_lifeCycleState) + switch (_requestState) { case EXPIRING: case ASYNC: - _lifeCycleState = _sendError ? LifeCycleState.BLOCKING : LifeCycleState.COMPLETE; + _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE; break; case COMPLETE: @@ -721,10 +721,10 @@ public void complete() default: throw new IllegalStateException(this.getStatusStringLocked()); } - if (_state == State.WAITING) + if (_state == HttpChannelState.State.WAITING) { handle = true; - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; } } @@ -743,7 +743,7 @@ public void asyncError(Throwable failure) AsyncContextEvent event = null; synchronized (this) { - if (_state == State.WAITING && _lifeCycleState == LifeCycleState.ASYNC) + if (_state == HttpChannelState.State.WAITING && _requestState == RequestState.ASYNC) { _event.addThrowable(failure); event = _event; @@ -814,7 +814,7 @@ else if (cause instanceof UnavailableException) LOG.debug("thrownException " + getStatusStringLocked(), th); // This can only be called from within the handle loop - if (_state != State.HANDLING) + if (_state != HttpChannelState.State.HANDLING) throw new IllegalStateException(getStatusStringLocked()); if (_sendError) @@ -824,7 +824,7 @@ else if (cause instanceof UnavailableException) } // Check async state to determine type of handling - switch (_lifeCycleState) + switch (_requestState) { case BLOCKING: // handle the exception with a sendError @@ -842,7 +842,7 @@ else if (cause instanceof UnavailableException) if (_asyncListeners == null || _asyncListeners.isEmpty()) { sendError.run(); - _lifeCycleState = LifeCycleState.BLOCKING; + _requestState = RequestState.BLOCKING; return; } asyncEvent = _event; @@ -851,7 +851,7 @@ else if (cause instanceof UnavailableException) break; default: - LOG.warn("unhandled in state " + _lifeCycleState, new IllegalStateException(th)); + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); return; } } @@ -881,13 +881,13 @@ else if (cause instanceof UnavailableException) if (_sendError) return; - switch (_lifeCycleState) + switch (_requestState) { case ASYNC: // The listeners did not invoke API methods // and the container must provide a default error dispatch. sendError.run(); - _lifeCycleState = LifeCycleState.BLOCKING; + _requestState = RequestState.BLOCKING; return; case DISPATCH: @@ -896,7 +896,7 @@ else if (cause instanceof UnavailableException) return; default: - LOG.warn("unhandled in state " + _lifeCycleState, new IllegalStateException(th)); + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); return; } } @@ -918,8 +918,8 @@ public void sendError(int code, String message) synchronized (this) { - if (_responseState != ResponseState.OPEN) - throw new IllegalStateException("Response is " + _responseState); + if (_outputState != OutputState.OPEN) + throw new IllegalStateException("Response is " + _outputState); response.resetContent(); // will throw ISE if committed response.getHttpOutput().sendErrorClose(); @@ -939,12 +939,12 @@ public void sendError(int code, String message) switch (_state) { case HANDLING: - case WAKING: + case WOKEN: case WAITING: _sendError = true; if (_event != null) { - Throwable cause = (Throwable) request.getAttribute(ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); if (cause != null) _event.addThrowable(cause); } @@ -965,12 +965,12 @@ protected void completing() if (LOG.isDebugEnabled()) LOG.debug("completing {}", toStringLocked()); - switch (_lifeCycleState) + switch (_requestState) { case COMPLETED: throw new IllegalStateException(getStatusStringLocked()); default: - _lifeCycleState = LifeCycleState.COMPLETING; + _requestState = RequestState.COMPLETING; } } } @@ -986,16 +986,19 @@ protected void completed() if (LOG.isDebugEnabled()) LOG.debug("completed {}", toStringLocked()); - switch (_lifeCycleState) + switch (_requestState) { case COMPLETING: if (_event == null) { - _lifeCycleState = LifeCycleState.COMPLETED; + _requestState = RequestState.COMPLETED; aListeners = null; event = null; - if (_state == State.WAITING) + if (_state == HttpChannelState.State.WAITING) + { + _state = State.WOKEN; handle = true; + } } else { @@ -1034,8 +1037,8 @@ protected void completed() synchronized (this) { - _lifeCycleState = LifeCycleState.COMPLETED; - if (_state == State.WAITING) + _requestState = RequestState.COMPLETED; + if (_state == HttpChannelState.State.WAITING) handle = true; } } @@ -1062,11 +1065,11 @@ protected void recycle() break; } _asyncListeners = null; - _state = State.IDLE; - _lifeCycleState = LifeCycleState.BLOCKING; - _responseState = ResponseState.OPEN; + _state = HttpChannelState.State.IDLE; + _requestState = RequestState.BLOCKING; + _outputState = OutputState.OPEN; _initial = true; - _asyncReadState = AsyncReadState.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -1089,10 +1092,10 @@ public void upgrade() throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; - _state = State.UPGRADED; - _lifeCycleState = LifeCycleState.BLOCKING; + _state = HttpChannelState.State.UPGRADED; + _requestState = RequestState.BLOCKING; _initial = true; - _asyncReadState = AsyncReadState.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -1124,7 +1127,7 @@ public boolean isIdle() { synchronized (this) { - return _state == State.IDLE; + return _state == HttpChannelState.State.IDLE; } } @@ -1133,7 +1136,7 @@ public boolean isExpired() synchronized (this) { // TODO review - return _lifeCycleState == LifeCycleState.EXPIRE || _lifeCycleState == LifeCycleState.EXPIRING; + return _requestState == RequestState.EXPIRE || _requestState == RequestState.EXPIRING; } } @@ -1149,7 +1152,7 @@ public boolean isSuspended() { synchronized (this) { - return _state == State.WAITING || _state == State.HANDLING && _lifeCycleState == LifeCycleState.ASYNC; + return _state == HttpChannelState.State.WAITING || _state == HttpChannelState.State.HANDLING && _requestState == RequestState.ASYNC; } } @@ -1157,7 +1160,7 @@ boolean isCompleted() { synchronized (this) { - return _lifeCycleState == LifeCycleState.COMPLETED; + return _requestState == RequestState.COMPLETED; } } @@ -1165,17 +1168,9 @@ public boolean isAsyncStarted() { synchronized (this) { - if (_state == State.HANDLING) - return _lifeCycleState != LifeCycleState.BLOCKING; - return _lifeCycleState == LifeCycleState.ASYNC || _lifeCycleState == LifeCycleState.EXPIRING; - } - } - - public boolean isAsyncComplete() - { - synchronized (this) - { - return _lifeCycleState == LifeCycleState.COMPLETE; + if (_state == HttpChannelState.State.HANDLING) + return _requestState != RequestState.BLOCKING; + return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING; } } @@ -1183,7 +1178,7 @@ public boolean isAsync() { synchronized (this) { - return !_initial || _lifeCycleState != LifeCycleState.BLOCKING; + return !_initial || _requestState != RequestState.BLOCKING; } } @@ -1274,18 +1269,18 @@ public void onReadUnready() if (LOG.isDebugEnabled()) LOG.debug("onReadUnready {}", toStringLocked()); - switch (_asyncReadState) + switch (_inputState) { case IDLE: case READY: - if (_state == State.WAITING) + if (_state == HttpChannelState.State.WAITING) { interested = true; - _asyncReadState = AsyncReadState.REGISTERED; + _inputState = InputState.REGISTERED; } else { - _asyncReadState = AsyncReadState.REGISTER; + _inputState = InputState.REGISTER; } break; @@ -1317,23 +1312,23 @@ public boolean onContentAdded() if (LOG.isDebugEnabled()) LOG.debug("onContentAdded {}", toStringLocked()); - switch (_asyncReadState) + switch (_inputState) { case IDLE: case READY: break; case PRODUCING: - _asyncReadState = AsyncReadState.READY; + _inputState = InputState.READY; break; case REGISTER: case REGISTERED: - _asyncReadState = AsyncReadState.READY; - if (_state == State.WAITING) + _inputState = InputState.READY; + if (_state == HttpChannelState.State.WAITING) { woken = true; - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; } break; @@ -1360,14 +1355,14 @@ public boolean onReadReady() if (LOG.isDebugEnabled()) LOG.debug("onReadReady {}", toStringLocked()); - switch (_asyncReadState) + switch (_inputState) { case IDLE: - _asyncReadState = AsyncReadState.READY; - if (_state == State.WAITING) + _inputState = InputState.READY; + if (_state == HttpChannelState.State.WAITING) { woken = true; - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; } break; @@ -1393,14 +1388,14 @@ public boolean onReadPossible() if (LOG.isDebugEnabled()) LOG.debug("onReadPossible {}", toStringLocked()); - switch (_asyncReadState) + switch (_inputState) { case REGISTERED: - _asyncReadState = AsyncReadState.POSSIBLE; - if (_state == State.WAITING) + _inputState = InputState.POSSIBLE; + if (_state == HttpChannelState.State.WAITING) { woken = true; - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; } break; @@ -1426,11 +1421,11 @@ public boolean onReadEof() LOG.debug("onEof {}", toStringLocked()); // Force read ready so onAllDataRead can be called - _asyncReadState = AsyncReadState.READY; - if (_state == State.WAITING) + _inputState = InputState.READY; + if (_state == HttpChannelState.State.WAITING) { woken = true; - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; } } return woken; @@ -1446,9 +1441,9 @@ public boolean onWritePossible() LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == State.WAITING) + if (_state == HttpChannelState.State.WAITING) { - _state = State.WAKING; + _state = HttpChannelState.State.WOKEN; wake = true; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 8a27b21ed1c4..0a9208e97221 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -64,6 +64,27 @@ public class HttpOutput extends ServletOutputStream implements Runnable private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + /* + ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED + -------------------------------------------------------------------------------------------------- + setWriteListener() READY->owp ise ise ise ise ise ise + write() OPEN ise PENDING wpe wpe eof eof + flush() OPEN ise PENDING wpe wpe eof eof + close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED + isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true + write completed - - - ASYNC READY->owp CLOSED - + */ + enum State + { + OPEN, // Open in blocking mode + ASYNC, // Open in async mode + READY, // isReady() has returned true + PENDING, // write operating in progress + UNREADY, // write operating in progress, isReady has returned false + ERROR, // An error has occured + CLOSING, // Asynchronous close in progress + CLOSED // Closed + } /** * The HttpOutput.Interceptor is a single intercept point for all @@ -143,22 +164,7 @@ default void resetBuffer() throws IllegalStateException private volatile Throwable _onError; private Callback _closeCallback; - /* - ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED - -------------------------------------------------------------------------------------------------- - setWriteListener() READY->owp ise ise ise ise ise ise - write() OPEN ise PENDING wpe wpe eof eof - flush() OPEN ise PENDING wpe wpe eof eof - close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED - isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true - write completed - - - ASYNC READY->owp CLOSED - - */ - private enum OutputState - { - OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSING, CLOSED - } - - private final AtomicReference _state = new AtomicReference<>(OutputState.OPEN); + private final AtomicReference _state = new AtomicReference<>(State.OPEN); public HttpOutput(HttpChannel channel) { @@ -202,7 +208,7 @@ public long getWritten() public void reopen() { - _state.set(OutputState.OPEN); + _state.set(State.OPEN); } private boolean isLastContentToWrite(int len) @@ -262,13 +268,13 @@ public void sendErrorClose() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case OPEN: case READY: case ASYNC: - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) continue; return; @@ -278,7 +284,10 @@ public void sendErrorClose() } } - + /** + * Make {@link #close()} am asynchronous method + * @param closeCallback The callback to use when close() is called. + */ public void setClosedCallback(Callback closeCallback) { _closeCallback = closeCallback; @@ -297,7 +306,7 @@ public void close() while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case CLOSING: @@ -313,7 +322,7 @@ public void close() // However it is desirable to allow a close at any time, specially if // complete is called. Thus we simulate a call to isReady here, assuming // that we can transition to READY. - _state.compareAndSet(state, OutputState.READY); + _state.compareAndSet(state, State.READY); continue; } case UNREADY: @@ -325,7 +334,7 @@ public void close() // complete is called. Because the prior write has not yet completed // and/or isReady has not been called, this close is allowed, but will // abort the response. - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) continue; IOException ex = new IOException("Closed while Pending/Unready"); LOG.warn(ex.toString()); @@ -336,7 +345,7 @@ public void close() } default: { - if (!_state.compareAndSet(state, OutputState.CLOSING)) + if (!_state.compareAndSet(state, State.CLOSING)) continue; // Do a normal close by writing the aggregate buffer or an empty buffer. If we are @@ -374,7 +383,7 @@ public void closed() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case CLOSED: @@ -383,13 +392,13 @@ public void closed() } case UNREADY: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) _writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); break; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) break; // Just make sure write and output stream really are closed @@ -454,7 +463,7 @@ public void flush() throws IOException { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case OPEN: @@ -465,7 +474,7 @@ public void flush() throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(state, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; new AsyncFlush().iterate(); return; @@ -493,7 +502,7 @@ public void write(byte[] b, int off, int len) throws IOException // Async or Blocking ? while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case OPEN: @@ -504,7 +513,7 @@ public void write(byte[] b, int off, int len) throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(state, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Should we aggregate? @@ -520,7 +529,7 @@ public void write(byte[] b, int off, int len) throws IOException // return if we are not complete, not full and filled all the content if (filled == len && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) throw new IllegalStateException(_state.get().toString()); return; } @@ -617,7 +626,7 @@ public void write(ByteBuffer buffer) throws IOException // Async or Blocking ? while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case OPEN: @@ -628,7 +637,7 @@ public void write(ByteBuffer buffer) throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(state, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Do the asynchronous writing from the callback @@ -693,7 +702,7 @@ public void write(int b) throws IOException throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(State.READY, State.PENDING)) continue; if (_aggregate == null) @@ -703,7 +712,7 @@ public void write(int b) throws IOException // Check if all written or full if (!complete && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) throw new IllegalStateException(); return; } @@ -1012,7 +1021,7 @@ public void sendContent(HttpContent httpContent, Callback callback) switch (_state.get()) { case OPEN: - if (!_state.compareAndSet(OutputState.OPEN, OutputState.PENDING)) + if (!_state.compareAndSet(State.OPEN, State.PENDING)) continue; break; @@ -1138,7 +1147,7 @@ public void setWriteListener(WriteListener writeListener) if (!_channel.getState().isAsync()) throw new IllegalStateException("!ASYNC"); - if (_state.compareAndSet(OutputState.OPEN, OutputState.READY)) + if (_state.compareAndSet(State.OPEN, State.READY)) { _writeListener = writeListener; if (_channel.getState().onWritePossible()) @@ -1163,12 +1172,12 @@ public boolean isReady() return true; case ASYNC: - if (!_state.compareAndSet(OutputState.ASYNC, OutputState.READY)) + if (!_state.compareAndSet(State.ASYNC, State.READY)) continue; return true; case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) + if (!_state.compareAndSet(State.PENDING, State.UNREADY)) continue; return false; @@ -1186,7 +1195,7 @@ public void run() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); if (_onError != null) { @@ -1201,7 +1210,7 @@ public void run() } default: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) { Throwable th = _onError; _onError = null; @@ -1277,16 +1286,16 @@ protected void onCompleteSuccess() { while (true) { - OutputState last = _state.get(); + State last = _state.get(); switch (last) { case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) continue; break; case UNREADY: - if (!_state.compareAndSet(OutputState.UNREADY, OutputState.READY)) + if (!_state.compareAndSet(State.UNREADY, State.READY)) continue; if (_last) closed(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index dd8e172a5e84..507020a71306 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -134,7 +134,6 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques break; } } - baseRequest.getResponse().closeOutput(); } /** @@ -253,6 +252,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques ByteBuffer content = bout.size() == 0 ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(bout.getBuf(), 0, bout.size()); baseRequest.getHttpChannel().sendCompleteResponse(content); + return; } default: diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java new file mode 100644 index 000000000000..142f86726170 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -0,0 +1,220 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.io.ChannelEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Scheduler; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Extended Server Tester. + */ +public class AsyncCompletionTest extends HttpServerTestFixture +{ + private static final Exchanger X = new Exchanger<>(); + private static final AtomicBoolean COMPLETE = new AtomicBoolean(); + + private static class DelayedCallback extends Callback.Nested + { + private CompletableFuture _delay = new CompletableFuture<>(); + + public DelayedCallback(Callback callback) + { + super(callback); + } + + @Override + public void succeeded() + { + _delay.complete(null); + } + + @Override + public void failed(Throwable x) + { + _delay.completeExceptionally(x); + } + + public void proceed() + { + try + { + _delay.get(10, TimeUnit.SECONDS); + getCallback().succeeded(); + } + catch(Throwable th) + { + th.printStackTrace(); + getCallback().failed(th); + } + } + } + + + @BeforeEach + public void init() throws Exception + { + COMPLETE.set(false); + + startServer(new ServerConnector(_server, new HttpConnectionFactory() + { + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint); + } + }) + { + @Override + protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + { + return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); + } + }); + } + + private static class ExtendedEndPoint extends SocketChannelEndPoint + { + public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + { + super(channel, selector, key, scheduler); + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException + { + DelayedCallback delay = new DelayedCallback(callback); + super.write(delay, buffers); + try + { + X.exchange(delay); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + } + + private static class ExtendedHttpConnection extends HttpConnection + { + public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint) + { + super(config, connector, endPoint, HttpCompliance.RFC7230_LEGACY, false); + } + + @Override + public void onCompleted() + { + COMPLETE.compareAndSet(false,true); + super.onCompleted(); + } + } + + // Tests from here use these parameters + public static Stream tests() + { + List tests = new ArrayList<>(); + tests.add(new Object[]{new HelloWorldHandler(), 200, "Hello world"}); + tests.add(new Object[]{new SendErrorHandler(499,"Test async sendError"), 499, "Test async sendError"}); + return tests.stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("tests") + public void testAsyncCompletion(Handler handler, int status, String message) throws Exception + { + configureServer(handler); + + int base = _threadPool.getBusyThreads(); + try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) + { + OutputStream os = client.getOutputStream(); + + // write the request + os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + + // The write should happen but the callback is delayed + HttpTester.Response response = HttpTester.parseResponse(client.getInputStream()); + assertThat(response.getStatus(), is(status)); + String content = response.getContent(); + assertThat(content, containsString(message)); + + // Check that a thread is held busy in write + assertThat(_threadPool.getBusyThreads(), Matchers.greaterThan(base)); + + // Getting the Delayed callback will free the thread + DelayedCallback delay = X.exchange(null, 10, TimeUnit.SECONDS); + + // wait for threads to return to base level + long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while(_threadPool.getBusyThreads() != base) + { + if (System.nanoTime() > end) + throw new TimeoutException(); + Thread.sleep(10); + } + + // We are now asynchronously waiting! + assertThat(COMPLETE.get(), is(false)); + + // proceed with the completion + delay.proceed(); + + while(!COMPLETE.get()) + { + if (System.nanoTime() > end) + throw new TimeoutException(); + Thread.sleep(10); + } + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java index 96a17208dd52..bd009b30d24a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java @@ -40,7 +40,8 @@ import org.junit.jupiter.api.BeforeEach; public class HttpServerTestFixture -{ // Useful constants +{ + // Useful constants protected static final long PAUSE = 10L; protected static final int LOOPS = 50; @@ -186,6 +187,31 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques } } + + protected static class SendErrorHandler extends AbstractHandler + { + private final int code; + private final String message; + + public SendErrorHandler() + { + this(500, null); + } + + public SendErrorHandler(int code, String message) + { + this.code = code; + this.message = message; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.sendError(code, message); + } + } + protected static class ReadExactHandler extends AbstractHandler.ErrorDispatchHandler { private int expected; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index e318b2ee788f..4cb4f63a48a5 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -693,7 +693,7 @@ public void testStatusCodes(int code, String message, String expectedMessage) th assertEquals(code, response.getStatus()); assertEquals(null, response.getReason()); assertEquals(expectedMessage, response.getHttpChannel().getRequest().getAttribute(RequestDispatcher.ERROR_MESSAGE)); - assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.ERROR_DISPATCH)); + assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.SEND_ERROR)); assertThat(response.getHttpChannel().getState().unhandle(), is(HttpChannelState.Action.COMPLETE)); } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index 7b948213a3f4..eb38c77dbee4 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -1095,20 +1095,20 @@ private static void appendDebugString(StringBuilder buf, ByteBuffer buffer) for (int i = 0; i < buffer.position(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == 16 && buffer.position() > 32) + if (i == 8 && buffer.position() > 16) { buf.append("..."); - i = buffer.position() - 16; + i = buffer.position() - 8; } } buf.append("<<<"); for (int i = buffer.position(); i < buffer.limit(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == buffer.position() + 16 && buffer.limit() > buffer.position() + 32) + if (i == buffer.position() + 24 && buffer.limit() > buffer.position() + 48) { buf.append("..."); - i = buffer.limit() - 16; + i = buffer.limit() - 24; } } buf.append(">>>"); @@ -1117,10 +1117,10 @@ private static void appendDebugString(StringBuilder buf, ByteBuffer buffer) for (int i = limit; i < buffer.capacity(); i++) { appendContentChar(buf, buffer.get(i)); - if (i == limit + 16 && buffer.capacity() > limit + 32) + if (i == limit + 8 && buffer.capacity() > limit + 16) { buf.append("..."); - i = buffer.capacity() - 16; + i = buffer.capacity() - 8; } } buffer.limit(limit); From 42979344175b5f9caaf6f6e8e1c2badfb6570513 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 1 Aug 2019 16:00:08 +1000 Subject: [PATCH 41/57] Cleanups and javadoc Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/server/HttpChannel.java | 15 +-- .../jetty/server/HttpChannelState.java | 125 +++++++++++------- .../org/eclipse/jetty/server/HttpInput.java | 3 +- .../org/eclipse/jetty/server/HttpOutput.java | 3 +- 4 files changed, 80 insertions(+), 66 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 5671b0f6d85c..918adeab4eaa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -25,13 +25,11 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; @@ -54,7 +52,6 @@ import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -623,9 +620,9 @@ else if (noStack != null) { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) - LOG.warn(_request.getRequestURI(), failure); + LOG.warn("handleException " + _request.getRequestURI(), failure); else - LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); + LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString()); } else { @@ -665,13 +662,7 @@ public void sendCompleteResponse(ByteBuffer content) { _request.setHandled(true); _state.completing(); - - final FuturePromise async = new FuturePromise<>(); - final AtomicBoolean written = new AtomicBoolean(); - sendResponse(null, content, true, Callback.from(() -> - { - _state.completed(); - })); + sendResponse(null, content, true, Callback.from(_state::completed)); } catch (Throwable x) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index af6f54d7e917..49825d027563 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -33,7 +33,6 @@ import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Scheduler; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; @@ -65,7 +64,22 @@ public enum State } /** - * The state of the servlet async API. + * The state of the request processing lifecycle. + *
+     *       BLOCKING <----> COMPLETING ---> COMPLETED
+     *       ^  |  ^            ^
+     *      /   |   \           |
+     *     |    |    DISPATCH   |
+     *     |    |    ^  ^       |
+     *     |    v   /   |       |
+     *     |  ASYNC -------> COMPLETE
+     *     |    |       |       ^
+     *     |    v       |       |
+     *     |  EXPIRE    |       |
+     *      \   |      /        |
+     *       \  v     /         |
+     *       EXPIRING ----------+
+     * 
*/ private enum RequestState { @@ -80,7 +94,7 @@ private enum RequestState } /** - * @see HttpInput.State + * The input readiness state, which works together with {@link HttpInput.State} */ private enum InputState { @@ -93,7 +107,7 @@ private enum InputState } /** - * @see HttpOutput.State + * The output committed state, which works together with {@link HttpOutput.State} */ private enum OutputState { @@ -123,10 +137,9 @@ public enum Action WAIT, // Wait for further events } - private final Locker _locker = new Locker(); private final HttpChannel _channel; private List _asyncListeners; - private State _state = HttpChannelState.State.IDLE; + private State _state = State.IDLE; private RequestState _requestState = RequestState.BLOCKING; private OutputState _outputState = OutputState.OPEN; private InputState _inputState = InputState.IDLE; @@ -229,7 +242,7 @@ public String toStringLocked() private String getStatusStringLocked() { - return String.format("s=%s lc=%s rs=%s ars=%s awp=%b se=%b i=%b al=%d", + return String.format("s=%s rs=%s os=%s is=%s awp=%b se=%b i=%b al=%d", _state, _requestState, _outputState, @@ -356,12 +369,15 @@ public Action handling() if (_requestState != RequestState.BLOCKING) throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = HttpChannelState.State.HANDLING; + _state = State.HANDLING; return Action.DISPATCH; case WOKEN: if (_event != null && _event.getThrowable() != null && !_sendError) + { + _state = State.HANDLING; return Action.ASYNC_ERROR; + } Action action = nextAction(true); if (LOG.isDebugEnabled()) @@ -415,7 +431,7 @@ protected Action unhandle() private Action nextAction(boolean handling) { // Assume we can keep going, but exceptions are below - _state = HttpChannelState.State.HANDLING; + _state = State.HANDLING; if (_sendError) { @@ -479,7 +495,7 @@ private Action nextAction(boolean handling) Scheduler scheduler = _channel.getScheduler(); if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - _state = HttpChannelState.State.WAITING; + _state = State.WAITING; return Action.WAIT; case DISPATCH: @@ -512,11 +528,11 @@ private Action nextAction(boolean handling) return Action.COMPLETE; case COMPLETING: - _state = HttpChannelState.State.WAITING; + _state = State.WAITING; return Action.WAIT; case COMPLETED: - _state = HttpChannelState.State.IDLE; + _state = State.IDLE; return Action.TERMINATED; default: @@ -532,7 +548,7 @@ public void startAsync(AsyncContextEvent event) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != HttpChannelState.State.HANDLING || _requestState != RequestState.BLOCKING) + if (_state != State.HANDLING || _requestState != RequestState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); _requestState = RequestState.ASYNC; @@ -611,7 +627,7 @@ public void dispatch(ServletContext context, String path) case WOKEN: break; case WAITING: - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; dispatch = true; break; default: @@ -638,9 +654,9 @@ protected void timeout() return; _requestState = RequestState.EXPIRE; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; dispatch = true; } } @@ -661,7 +677,7 @@ protected void onTimeout() { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - if (_requestState != RequestState.EXPIRING || _state != HttpChannelState.State.HANDLING) + if (_requestState != RequestState.EXPIRING || _state != State.HANDLING) throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; @@ -721,10 +737,10 @@ public void complete() default: throw new IllegalStateException(this.getStatusStringLocked()); } - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { handle = true; - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; } } @@ -743,8 +759,12 @@ public void asyncError(Throwable failure) AsyncContextEvent event = null; synchronized (this) { - if (_state == HttpChannelState.State.WAITING && _requestState == RequestState.ASYNC) + if (LOG.isDebugEnabled()) + LOG.debug("asyncError " + toStringLocked(), failure); + + if (_state == State.WAITING && _requestState == RequestState.ASYNC) { + _state = State.WOKEN; _event.addThrowable(failure); event = _event; } @@ -814,7 +834,7 @@ else if (cause instanceof UnavailableException) LOG.debug("thrownException " + getStatusStringLocked(), th); // This can only be called from within the handle loop - if (_state != HttpChannelState.State.HANDLING) + if (_state != State.HANDLING) throw new IllegalStateException(getStatusStringLocked()); if (_sendError) @@ -920,18 +940,8 @@ public void sendError(int code, String message) { if (_outputState != OutputState.OPEN) throw new IllegalStateException("Response is " + _outputState); - response.resetContent(); // will throw ISE if committed response.getHttpOutput().sendErrorClose(); - - request.getResponse().setStatus(code); - // we are allowed to have a body, then produce the error page. - ContextHandler.Context context = request.getErrorContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); - request.setAttribute(ERROR_STATUS_CODE, code); - request.setAttribute(ERROR_MESSAGE, message); + response.resetContent(); // will throw ISE if committed if (LOG.isDebugEnabled()) LOG.debug("sendError {}", toStringLocked()); @@ -940,7 +950,17 @@ public void sendError(int code, String message) { case HANDLING: case WOKEN: - case WAITING: + case WAITING: + request.getResponse().setStatus(code); + // we are allowed to have a body, then produce the error page. + ContextHandler.Context context = request.getErrorContext(); + if (context != null) + request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + _sendError = true; if (_event != null) { @@ -994,7 +1014,7 @@ protected void completed() _requestState = RequestState.COMPLETED; aListeners = null; event = null; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { _state = State.WOKEN; handle = true; @@ -1038,8 +1058,11 @@ protected void completed() synchronized (this) { _requestState = RequestState.COMPLETED; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) + { + _state = State.WOKEN; handle = true; + } } } @@ -1065,7 +1088,7 @@ protected void recycle() break; } _asyncListeners = null; - _state = HttpChannelState.State.IDLE; + _state = State.IDLE; _requestState = RequestState.BLOCKING; _outputState = OutputState.OPEN; _initial = true; @@ -1092,7 +1115,7 @@ public void upgrade() throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; - _state = HttpChannelState.State.UPGRADED; + _state = State.UPGRADED; _requestState = RequestState.BLOCKING; _initial = true; _inputState = InputState.IDLE; @@ -1127,7 +1150,7 @@ public boolean isIdle() { synchronized (this) { - return _state == HttpChannelState.State.IDLE; + return _state == State.IDLE; } } @@ -1152,7 +1175,7 @@ public boolean isSuspended() { synchronized (this) { - return _state == HttpChannelState.State.WAITING || _state == HttpChannelState.State.HANDLING && _requestState == RequestState.ASYNC; + return _state == State.WAITING || _state == State.HANDLING && _requestState == RequestState.ASYNC; } } @@ -1168,7 +1191,7 @@ public boolean isAsyncStarted() { synchronized (this) { - if (_state == HttpChannelState.State.HANDLING) + if (_state == State.HANDLING) return _requestState != RequestState.BLOCKING; return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING; } @@ -1273,7 +1296,7 @@ public void onReadUnready() { case IDLE: case READY: - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { interested = true; _inputState = InputState.REGISTERED; @@ -1325,10 +1348,10 @@ public boolean onContentAdded() case REGISTER: case REGISTERED: _inputState = InputState.READY; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { woken = true; - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; } break; @@ -1359,10 +1382,10 @@ public boolean onReadReady() { case IDLE: _inputState = InputState.READY; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { woken = true; - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; } break; @@ -1392,10 +1415,10 @@ public boolean onReadPossible() { case REGISTERED: _inputState = InputState.POSSIBLE; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { woken = true; - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; } break; @@ -1422,10 +1445,10 @@ public boolean onReadEof() // Force read ready so onAllDataRead can be called _inputState = InputState.READY; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { woken = true; - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; } } return woken; @@ -1441,9 +1464,9 @@ public boolean onWritePossible() LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == HttpChannelState.State.WAITING) + if (_state == State.WAITING) { - _state = HttpChannelState.State.WOKEN; + _state = State.WOKEN; wake = true; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index bb5b1d588331..15f04a2c8647 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -291,7 +291,8 @@ public int read(byte[] b, int off, int len) throws IOException { BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, String.format("Request content data rate < %d B/s", minRequestDataRate)); - _channelState.getHttpChannel().abort(bad); + if (_channelState.isResponseCommitted()) + _channelState.getHttpChannel().abort(bad); throw bad; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 0a9208e97221..5eb0c61a3ad6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -151,6 +151,7 @@ default void resetBuffer() throws IllegalStateException private static Logger LOG = Log.getLogger(HttpOutput.class); private static final ThreadLocal _encoder = new ThreadLocal<>(); + private final AtomicReference _state = new AtomicReference<>(State.OPEN); private final HttpChannel _channel; private final SharedBlockingCallback _writeBlocker; private Interceptor _interceptor; @@ -164,8 +165,6 @@ default void resetBuffer() throws IllegalStateException private volatile Throwable _onError; private Callback _closeCallback; - private final AtomicReference _state = new AtomicReference<>(State.OPEN); - public HttpOutput(HttpChannel channel) { _channel = channel; From 07203c3d33adaeb973816e524f9d3ac12f9356ac Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 1 Aug 2019 16:21:27 +1000 Subject: [PATCH 42/57] Cleanups and javadoc Signed-off-by: Greg Wilkins --- .../java/org/eclipse/jetty/server/HttpChannelState.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 49825d027563..fba272e41a03 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -53,6 +53,13 @@ public class HttpChannelState /** * The state of the HttpChannel,used to control the overall lifecycle. + *
+     *     IDLE <-----> HANDLING ----> WAITING
+     *       |                 ^       /
+     *       |                  \     /
+     *       v                   \   v
+     *    UPGRADED               WOKEN
+     * 
*/ public enum State { From d17b975866f692c238f5ae43ab231762ea9d4050 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Aug 2019 12:28:09 +1000 Subject: [PATCH 43/57] remove snake case Signed-off-by: Greg Wilkins --- .../server/ssl/SniSslConnectionFactoryTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index a24eda4337b4..21c31d77e847 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -80,23 +80,23 @@ public void before() throws Exception { _server = new Server(); - HttpConfiguration http_config = new HttpConfiguration(); - http_config.setSecureScheme("https"); - http_config.setSecurePort(8443); - http_config.setOutputBufferSize(32768); - _httpsConfig = new HttpConfiguration(http_config); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + httpConfig.setSecurePort(8443); + httpConfig.setOutputBufferSize(32768); + _httpsConfig = new HttpConfiguration(httpConfig); SecureRequestCustomizer src = new SecureRequestCustomizer(); src.setSniHostCheck(true); _httpsConfig.addCustomizer(src); - _httpsConfig.addCustomizer((connector, httpConfig, request) -> + _httpsConfig.addCustomizer((connector, hc, request) -> { EndPoint endp = request.getHttpChannel().getEndPoint(); if (endp instanceof SslConnection.DecryptedEndPoint) { try { - SslConnection.DecryptedEndPoint ssl_endp = (SslConnection.DecryptedEndPoint)endp; - SslConnection sslConnection = ssl_endp.getSslConnection(); + SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp; + SslConnection sslConnection = sslEndp.getSslConnection(); SSLEngine sslEngine = sslConnection.getSSLEngine(); SSLSession session = sslEngine.getSession(); for (Certificate c : session.getLocalCertificates()) From 525e294106c5d8db6c9d40e5ab04e9fa9903b4d6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Aug 2019 13:32:59 +1000 Subject: [PATCH 44/57] feedback from review Signed-off-by: Greg Wilkins --- .../java/org/eclipse/jetty/server/HttpChannelState.java | 2 +- .../src/main/java/org/eclipse/jetty/server/HttpOutput.java | 7 +++++-- .../org/eclipse/jetty/server/handler/ErrorHandler.java | 1 + .../eclipse/jetty/server/handler/NcsaRequestLogTest.java | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index fba272e41a03..4f6a6cf7621d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -239,7 +239,7 @@ public String toString() } } - public String toStringLocked() + private String toStringLocked() { return String.format("%s@%x{%s}", getClass().getSimpleName(), diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 5eb0c61a3ad6..f0cbe8f2d14b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -62,6 +62,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable { private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; + private static final Callback BLOCKING_CLOSE_CALLBACK = new Callback() {}; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); /* @@ -300,8 +301,10 @@ public Callback getClosedCallback() @Override public void close() { - Callback closeCallback = _closeCallback == null ? Callback.NOOP : _closeCallback; + Callback closeCallback = _closeCallback; _closeCallback = null; + if (closeCallback == null) + closeCallback = BLOCKING_CLOSE_CALLBACK; while (true) { @@ -352,7 +355,7 @@ public void close() try { ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER; - if (closeCallback == Callback.NOOP) + if (closeCallback == BLOCKING_CLOSE_CALLBACK) { // Do a blocking close write(content, !_channel.getResponse().isIncluding()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 507020a71306..585803ab29f3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -243,6 +243,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques // We will write it into a byte array buffer so // we can flush it asynchronously. + // TODO get a buffer from the buffer pool and return. ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); PrintWriter writer = new PrintWriter(new OutputStreamWriter(bout, charset)); response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java index 2230817880c7..e854f3f44f12 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java @@ -573,7 +573,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { request.setAttribute("ASYNC", Boolean.TRUE); AsyncContext ac = request.startAsync(); - ac.setTimeout(100000); + ac.setTimeout(1000); baseRequest.setHandled(true); _server.getThreadPool().execute(() -> { @@ -583,7 +583,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques { while (baseRequest.getHttpChannel().getState().getState() != HttpChannelState.State.WAITING) { - Thread.yield(); + Thread.sleep(10); } baseRequest.setHandled(false); testHandler.handle(target, baseRequest, request, response); From d70098edafa7830b21f1714b6c05e59b347b76fd Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 3 Aug 2019 12:15:13 +1000 Subject: [PATCH 45/57] Write error page into fixed pooled buffer Use the response to get/release a pooled buffer into which the error page can be written. Make it a fixed sized buffer and if it overflows then no error page is generated (first overflow turns off showstacks to save space). The ErrorHandler badly needs to be refactored, but we cannot change API in jetty-9 Signed-off-by: Greg Wilkins --- .../jetty/io/ByteBufferOutputStream.java | 62 +++++ .../org/eclipse/jetty/server/HttpChannel.java | 11 +- .../org/eclipse/jetty/server/HttpOutput.java | 28 +- .../org/eclipse/jetty/server/Response.java | 21 +- .../jetty/server/handler/ErrorHandler.java | 245 +++++++++++++----- .../jetty/server/ErrorHandlerTest.java | 38 --- .../eclipse/jetty/servlet/ErrorPageTest.java | 18 +- .../org/eclipse/jetty/util/BufferUtil.java | 1 + 8 files changed, 291 insertions(+), 133 deletions(-) create mode 100644 jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java new file mode 100644 index 000000000000..fc68e4714b90 --- /dev/null +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java @@ -0,0 +1,62 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; + +/** + * Simple wrapper of a ByteBuffer as an OutputStream. + * The buffer does not grow and this class will throw an + * {@link java.nio.BufferOverflowException} if the buffer capacity is exceeded. + */ +public class ByteBufferOutputStream extends OutputStream +{ + final ByteBuffer _buffer; + + public ByteBufferOutputStream(ByteBuffer buffer) + { + _buffer = buffer; + } + + public void close() + { + } + + public void flush() + { + } + + public void write(byte[] b) + { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) + { + BufferUtil.append(_buffer, b, off, len); + } + + public void write(int b) + { + BufferUtil.append(_buffer, (byte)b); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 918adeab4eaa..1da494fda5f9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -441,7 +441,7 @@ public boolean handle() errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - sendCompleteResponse(null); + sendCompleteResponse(); break; } @@ -494,7 +494,10 @@ public boolean handle() if (_state.isResponseCommitted()) abort(x); else - sendCompleteResponse(null); + { + _response.resetContent(); + sendCompleteResponse(); + } } finally { @@ -656,13 +659,13 @@ protected Throwable unwrap(Throwable failure, Class... targets) return null; } - public void sendCompleteResponse(ByteBuffer content) + public void sendCompleteResponse() { try { _request.setHandled(true); _state.completing(); - sendResponse(null, content, true, Callback.from(_state::completed)); + sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed)); } catch (Throwable x) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index f0cbe8f2d14b..dddd5e0eded9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -322,8 +322,8 @@ public void close() // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. // However it is desirable to allow a close at any time, specially if - // complete is called. Thus we simulate a call to isReady here, assuming - // that we can transition to READY. + // complete is called. Thus we simulate a call to isReady here, by + // trying to move to READY state. Either way we continue. _state.compareAndSet(state, State.READY); continue; } @@ -425,6 +425,18 @@ public void closed() } } + public ByteBuffer getBuffer() + { + return _aggregate; + } + + public ByteBuffer acquireBuffer() + { + if (_aggregate == null) + _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + return _aggregate; + } + private void releaseBuffer() { if (_aggregate != null) @@ -522,8 +534,7 @@ public void write(byte[] b, int off, int len) throws IOException boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -569,8 +580,7 @@ public void write(byte[] b, int off, int len) throws IOException boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(capacity, _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -691,8 +701,7 @@ public void write(int b) throws IOException switch (_state.get()) { case OPEN: - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full @@ -707,8 +716,7 @@ public void write(int b) throws IOException if (!_state.compareAndSet(State.READY, State.PENDING)) continue; - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index e43eec2b6fe3..0a1ca984ccec 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -995,23 +995,18 @@ public void flushBuffer() throws IOException _out.flush(); } - private void resetStatusAndFields() + @Override + public void reset() { - _out.resetBuffer(); - _outputType = OutputType.NONE; _status = 200; _reason = null; + _out.resetBuffer(); + _outputType = OutputType.NONE; _contentLength = -1; _contentType = null; _mimeType = null; _characterEncoding = null; _encodingFrom = EncodingFrom.NOT_SET; - } - - @Override - public void reset() - { - resetStatusAndFields(); // Clear all response headers _fields.clear(); @@ -1056,7 +1051,13 @@ public void reset() public void resetContent() { - resetStatusAndFields(); + _out.resetBuffer(); + _outputType = OutputType.NONE; + _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; // remove the content related response headers and keep all others for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 585803ab29f3..52549c9d153a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -21,8 +21,8 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.StringWriter; import java.io.Writer; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -36,11 +36,12 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.QuotedQualityCSV; +import org.eclipse.jetty.io.ByteBufferOutputStream; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.ByteArrayOutputStream2; +import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -54,6 +55,7 @@ */ public class ErrorHandler extends AbstractHandler { + // TODO This classes API needs to be majorly refactored/cleanup in jetty-10 private static final Logger LOG = Log.getLogger(ErrorHandler.class); public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; @@ -130,7 +132,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) // TODO revisit this fix AFTER implemented delayed dispatch + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) break; } } @@ -205,60 +207,111 @@ protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest req protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType) throws IOException { + // We can generate an acceptable contentType, but can we generate an acceptable charset? + // TODO refactor this in jetty-10 to be done in the other calling loop + Charset charset = null; + List acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); + if (!acceptable.isEmpty()) + { + for (String name : acceptable) + { + if ("*".equals(name)) + { + charset = StandardCharsets.UTF_8; + break; + } + + try + { + charset = Charset.forName(name); + } + catch (Exception e) + { + LOG.ignore(e); + } + } + if (charset == null) + return; + } + + MimeTypes.Type type; switch (contentType) { case "text/html": case "text/*": case "*/*": - { - // We can generate an acceptable contentType, but can we generate an acceptable charset? - Charset charset = null; - List acceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); - if (acceptable.isEmpty()) + type = MimeTypes.Type.TEXT_HTML; + if (charset == null) charset = StandardCharsets.ISO_8859_1; - else - { - for (String name : acceptable) - { - if ("*".equals(name)) - { - charset = StandardCharsets.UTF_8; - break; - } - - try - { - charset = Charset.forName(name); - } - catch (Exception e) - { - LOG.ignore(e); - } - } - } + break; - // If we have no acceptable charset, don't write an error page. + case "text/json": + case "application/json": + type = MimeTypes.Type.TEXT_JSON; if (charset == null) - return; - - // We will write it into a byte array buffer so - // we can flush it asynchronously. - // TODO get a buffer from the buffer pool and return. - ByteArrayOutputStream2 bout = new ByteArrayOutputStream2(1024); - PrintWriter writer = new PrintWriter(new OutputStreamWriter(bout, charset)); - response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); - response.setCharacterEncoding(charset.name()); - handleErrorPage(request, writer, code, message); - writer.flush(); - ByteBuffer content = bout.size() == 0 ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(bout.getBuf(), 0, bout.size()); + charset = StandardCharsets.UTF_8; + break; - baseRequest.getHttpChannel().sendCompleteResponse(content); - return; - } + case "text/plain": + type = MimeTypes.Type.TEXT_PLAIN; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; default: return; } + + // We will write it into a byte array buffer so + // we can flush it asynchronously. + while(true) + { + try + { + ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer(); + ByteBufferOutputStream out = new ByteBufferOutputStream(buffer); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset)); + + switch (type) + { + case TEXT_HTML: + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + response.setCharacterEncoding(charset.name()); + handleErrorPage(request, writer, code, message); + break; + case TEXT_JSON: + response.setContentType(contentType); + writeErrorJson(request, writer, code, message); + break; + case TEXT_PLAIN: + response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString()); + response.setCharacterEncoding(charset.name()); + writeErrorPlain(request, writer, code, message); + break; + default: + throw new IllegalStateException(); + } + + writer.flush(); + break; + } + catch (BufferOverflowException e) + { + LOG.warn("Error page too large: {} {} {}", code, message, request); + if (LOG.isDebugEnabled()) + LOG.warn(e); + baseRequest.getResponse().resetContent(); + if (_showStacks) + { + LOG.info("Disabling showsStacks for " + this); + _showStacks = false; + continue; + } + break; + } + } + + baseRequest.getHttpChannel().sendCompleteResponse(); } protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) @@ -285,12 +338,13 @@ protected void writeErrorPageHead(HttpServletRequest request, Writer writer, int { writer.write("\n"); writer.write("Error "); - writer.write(Integer.toString(code)); - - if (_showMessageInTitle) + // TODO this code is duplicated in writeErrorPageMessage + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) { writer.write(' '); - write(writer, message); + writer.write(StringUtil.sanitizeXmlString(message)); } writer.write("\n"); } @@ -312,29 +366,96 @@ protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, throws IOException { writer.write("

HTTP ERROR "); + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) + { + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + } + writer.write("

\n"); + writer.write("\n"); + htmlRow(writer, "URI", uri); + htmlRow(writer, "STATUS", status); + htmlRow(writer, "MESSAGE", message); + htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + htmlRow(writer, "CAUSED BY", cause); + cause = cause.getCause(); + } + writer.write("
\n"); + } + + private void htmlRow(Writer writer, String tag, Object value) + throws IOException + { + writer.write(""); + writer.write(tag); + writer.write(":"); + if (value == null) + writer.write("-"); + else + writer.write(StringUtil.sanitizeXmlString(value.toString())); + writer.write("\n"); + } + + private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer.write("HTTP ERROR "); writer.write(Integer.toString(code)); - writer.write("\n

Problem accessing "); - write(writer, uri); - writer.write(". Reason:\n

    ");
-        write(writer, message);
-        writer.write("

"); + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + writer.write("\n"); + writer.printf("URI: %s%n", request.getRequestURI()); + writer.printf("STATUS: %s%n", code); + writer.printf("MESSAGE: %s%n", message); + writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + writer.printf("CAUSED BY %s%n", cause); + cause = cause.getCause(); + } + } + + private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer + .append("{\n") + .append(" url: \"").append(request.getRequestURI()).append("\",\n") + .append(" status: \"").append(Integer.toString(code)).append("\",\n") + .append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n"); + Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); + if (servlet !=null) + writer.append("servlet: \"").append(servlet.toString()).append("\",\n"); + Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + int c = 0; + while (cause != null) + { + writer.append(" cause").append(Integer.toString(c++)).append(": ") + .append(QuotedStringTokenizer.quote(cause.toString())).append(",\n"); + cause = cause.getCause(); + } + writer.append("}"); } + protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - while (th != null) + if (_showStacks && th != null) { - writer.write("

Caused by:

");
-            StringWriter sw = new StringWriter();
-            PrintWriter pw = new PrintWriter(sw);
-            th.printStackTrace(pw);
-            pw.flush();
-            write(writer, sw.getBuffer().toString());
+            PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer);
+            pw.write("
");
+            while (th != null)
+            {
+                th.printStackTrace(pw);
+                th = th.getCause();
+            }
             writer.write("
\n"); - - th = th.getCause(); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java index 0f5369036aef..7e396b072b27 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java @@ -30,7 +30,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,43 +52,6 @@ public static void before() throws Exception server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); - server.addBean(new ErrorHandler() - { - @Override - protected void generateAcceptableResponse( - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response, - int code, - String message, - String mimeType) throws IOException - { - switch (mimeType) - { - case "text/json": - case "application/json": - { - baseRequest.setHandled(true); - response.setContentType(mimeType); - response.getWriter() - .append("{") - .append("code: \"").append(Integer.toString(code)).append("\",") - .append("message: \"").append(message).append('"') - .append("}"); - break; - } - case "text/plain": - { - baseRequest.setHandled(true); - response.setContentType("text/plain"); - response.getOutputStream().print(response.getContentType()); - break; - } - default: - super.generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - } - } - }); server.setHandler(new AbstractHandler() { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 61d2cd2d1df4..0d8db7f6e55b 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -186,9 +186,9 @@ void testGenerateAcceptableResponse_noAcceptHeader() throws Exception String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); - assertThat(response, Matchers.containsString("Error 598 598")); - assertThat(response, Matchers.containsString("

HTTP ERROR 598

")); - assertThat(response, Matchers.containsString("Problem accessing /fail/code. Reason:")); + assertThat(response, Matchers.containsString("Error 598")); + assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598")); + assertThat(response, Matchers.containsString("/fail/code")); } @Test @@ -201,9 +201,9 @@ void testGenerateAcceptableResponse_htmlAcceptHeader() throws Exception String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + "Accept: application/bytes,text/html\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); - assertThat(response, Matchers.containsString("<title>Error 598 598")); - assertThat(response, Matchers.containsString("

HTTP ERROR 598

")); - assertThat(response, Matchers.containsString("Problem accessing /fail/code. Reason:")); + assertThat(response, Matchers.containsString("Error 598")); + assertThat(response, Matchers.containsString("<h2>HTTP ERROR 598")); + assertThat(response, Matchers.containsString("/fail/code")); } @Test @@ -215,9 +215,9 @@ void testGenerateAcceptableResponse_noHtmlAcceptHeader() throws Exception String response = _connector.getResponse("GET /fail/code?code=598 HTTP/1.0\r\n" + "Accept: application/bytes\r\n\r\n"); assertThat(response, Matchers.containsString("HTTP/1.1 598 598")); - assertThat(response, not(Matchers.containsString("<title>Error 598 598"))); - assertThat(response, not(Matchers.containsString("

HTTP ERROR 598

"))); - assertThat(response, not(Matchers.containsString("Problem accessing /fail/code. Reason:"))); + assertThat(response, not(Matchers.containsString("Error 598"))); + assertThat(response, not(Matchers.containsString("<h2>HTTP ERROR 598"))); + assertThat(response, not(Matchers.containsString("/fail/code"))); } @Test diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index 496a5f354608..452c1be848c0 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -440,6 +440,7 @@ public static void append(ByteBuffer to, byte[] b, int off, int len) throws Buff * * @param to Buffer is flush mode * @param b byte to append + * @throws BufferOverflowException if unable to append buffer due to space limits */ public static void append(ByteBuffer to, byte b) { From 48e6289720885d34f6838f5120d75e1856a482e7 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Sun, 4 Aug 2019 08:35:12 +1000 Subject: [PATCH 46/57] More test fixes for different error page format Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/tests/distribution/BadAppTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java index 190bc381743b..9248a2dc5e40 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java @@ -108,8 +108,8 @@ public void testXml_ThrowOnUnavailable_False() throws Exception startHttpClient(); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Unavailable")); - assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>")); + assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>")); } } } @@ -148,8 +148,8 @@ public void testNoXml_ThrowOnUnavailable_Default() throws Exception startHttpClient(); ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Unavailable")); - assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + assertThat(response.getContentAsString(), containsString("<h2>HTTP ERROR 503 Service Unavailable</h2>")); + assertThat(response.getContentAsString(), containsString("<tr><th>URI:</th><td>/badapp/</td></tr>")); } } } From 64e7276d7dcec0f84d759a6dac7ebf5a168ddcb0 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Sun, 4 Aug 2019 11:16:19 +1000 Subject: [PATCH 47/57] minor cleanups Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../jetty/proxy/AbstractProxyServlet.java | 17 ++++++------ .../jetty/rewrite/handler/ValidUrlRule.java | 8 ++++-- .../jetty/server/HttpChannelState.java | 26 ++++--------------- .../jetty/server/handler/ContextHandler.java | 3 +-- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index c2c45ace64ca..c670df04766a 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -675,21 +675,20 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ proxyResponse.resetBuffer(); proxyResponse.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString()); } + proxyResponse.sendError(status); + } + catch (Exception e) + { + _log.ignore(e); try { - proxyResponse.sendError(status); + proxyResponse.sendError(-1); } - catch (IllegalStateException e) + catch(Exception e2) { - _log.ignore(e); - // Abort connection instead - proxyResponse.sendError(-1); + _log.ignore(e2); } } - catch (Exception e) - { - _log.ignore(e); - } finally { if (clientRequest.isAsyncStarted()) diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java index 3ff48c8dcd63..1e3207bb7dcc 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ValidUrlRule.java @@ -90,9 +90,13 @@ public String matchAndApply(String target, HttpServletRequest request, HttpServl // status code 400 and up are error codes so include a reason if (code >= 400) { - response.sendError(code); - if (!StringUtil.isBlank(_reason)) + if (StringUtil.isBlank(_reason)) + response.sendError(code); + else + { Request.getBaseRequest(request).getResponse().setStatusWithReason(code, _reason); + response.sendError(code, _reason); + } } else { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 4f6a6cf7621d..73d78e096497 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -605,43 +605,27 @@ public void dispatch(ServletContext context, String path) if (LOG.isDebugEnabled()) LOG.debug("dispatch {} -> {}", toStringLocked(), path); - // TODO this method can be simplified - - boolean started = false; - event = _event; switch (_requestState) { case ASYNC: - started = true; - break; case EXPIRING: break; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _requestState = RequestState.DISPATCH; if (context != null) _event.setDispatchContext(context); if (path != null) _event.setDispatchPath(path); - if (started) + if (_requestState == RequestState.ASYNC && _state == State.WAITING) { - switch (_state) - { - case HANDLING: - case WOKEN: - break; - case WAITING: - _state = State.WOKEN; - dispatch = true; - break; - default: - LOG.warn("async dispatched when complete {}", this); - break; - } + _state = State.WOKEN; + dispatch = true; } + _requestState = RequestState.DISPATCH; + event = _event; } cancelTimeout(event); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 1c07b18aa49a..afec04af9d79 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1138,7 +1138,6 @@ public void doScope(String target, Request baseRequest, HttpServletRequest reque if (oldContext != _scontext) { // check the target. - // TODO this is a fragile accord with the Dispatcher if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) { if (_compactPath) @@ -1278,8 +1277,8 @@ public void doHandle(String target, Request baseRequest, HttpServletRequest requ if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); baseRequest.setHandled(true); + response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } From 56e19ba820aef29c38f242e9244d6708dfe57f9a Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Mon, 5 Aug 2019 10:39:07 +1000 Subject: [PATCH 48/57] Cleanup from Review Fixed javadoc Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../java/org/eclipse/jetty/server/HttpChannelState.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 73d78e096497..66daa3fc332b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -51,7 +51,7 @@ public class HttpChannelState private static final long DEFAULT_TIMEOUT = Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT", 30000L); - /** + /* * The state of the HttpChannel,used to control the overall lifecycle. * <pre> * IDLE <-----> HANDLING ----> WAITING @@ -70,7 +70,7 @@ public enum State UPGRADED // Request upgraded the connection } - /** + /* * The state of the request processing lifecycle. * <pre> * BLOCKING <----> COMPLETING ---> COMPLETED @@ -100,7 +100,7 @@ private enum RequestState COMPLETED // Response is completed } - /** + /* * The input readiness state, which works together with {@link HttpInput.State} */ private enum InputState @@ -113,7 +113,7 @@ private enum InputState READY // isReady() was false, onContentAdded has been called } - /** + /* * The output committed state, which works together with {@link HttpOutput.State} */ private enum OutputState From cbce985fc55890fbe85417bfa11e95fc8aded5b6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Mon, 5 Aug 2019 14:13:43 +1000 Subject: [PATCH 49/57] cleanups and simplifications Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/server/HttpChannel.java | 19 ++- .../jetty/server/HttpChannelState.java | 111 +++++++----------- .../jetty/server/handler/ErrorHandler.java | 9 +- .../jetty/server/AsyncCompletionTest.java | 1 + 4 files changed, 57 insertions(+), 83 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 1da494fda5f9..a29073aa673b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -346,10 +346,6 @@ public boolean handle() // break loop without calling unhandle break loop; - case NOOP: - // do nothing other than call unhandle - break; - case DISPATCH: { if (!_request.hasMetaData()) @@ -449,8 +445,15 @@ public boolean handle() String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; - if (errorDispatcher != null) + if (errorDispatcher == null) + { + // Allow ErrorHandler to generate response + errorHandler.handle(null, _request, _request, _response); + _request.setHandled(true); + } + else { + // Do the error page dispatch try { _request.setAttribute(ErrorHandler.ERROR_PAGE, errorPage); @@ -470,12 +473,6 @@ public boolean handle() _request.setDispatcherType(null); } } - else - { - // Allow ErrorHandler to generate response - errorHandler.handle(null, _request, _request, _response); - _request.setHandled(true); - } } catch (Throwable x) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 66daa3fc332b..38a6f1889965 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -129,7 +129,6 @@ private enum OutputState */ public enum Action { - NOOP, // No action DISPATCH, // handle a normal request dispatch ASYNC_DISPATCH, // handle an async request dispatch SEND_ERROR, // Generate an error page or error dispatch @@ -391,9 +390,6 @@ public Action handling() LOG.debug("nextAction(true) {} {}", action, toStringLocked()); return action; - case WAITING: - case HANDLING: - case UPGRADED: default: throw new IllegalStateException(getStatusStringLocked()); } @@ -417,14 +413,8 @@ protected Action unhandle() if (LOG.isDebugEnabled()) LOG.debug("unhandle {}", toStringLocked()); - switch (_state) - { - case HANDLING: - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } + if (_state != State.HANDLING) + throw new IllegalStateException(this.getStatusStringLocked()); _initial = false; @@ -477,12 +467,8 @@ private Action nextAction(boolean handling) return Action.READ_CALLBACK; case REGISTER: case PRODUCING: - if (!handling) - { - _inputState = InputState.REGISTERED; - return Action.READ_REGISTER; - } - break; + _inputState = InputState.REGISTERED; + return Action.READ_REGISTER; case IDLE: case REGISTERED: break; @@ -496,9 +482,6 @@ private Action nextAction(boolean handling) return Action.WRITE_CALLBACK; } - if (handling) - return Action.NOOP; - Scheduler scheduler = _channel.getScheduler(); if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); @@ -929,11 +912,6 @@ public void sendError(int code, String message) synchronized (this) { - if (_outputState != OutputState.OPEN) - throw new IllegalStateException("Response is " + _outputState); - response.getHttpOutput().sendErrorClose(); - response.resetContent(); // will throw ISE if committed - if (LOG.isDebugEnabled()) LOG.debug("sendError {}", toStringLocked()); @@ -941,30 +919,30 @@ public void sendError(int code, String message) { case HANDLING: case WOKEN: - case WAITING: - request.getResponse().setStatus(code); - // we are allowed to have a body, then produce the error page. - ContextHandler.Context context = request.getErrorContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); - request.setAttribute(ERROR_STATUS_CODE, code); - request.setAttribute(ERROR_MESSAGE, message); - - _sendError = true; - if (_event != null) - { - Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); - if (cause != null) - _event.addThrowable(cause); - } + case WAITING: break; - default: - { throw new IllegalStateException(getStatusStringLocked()); - } + } + if (_outputState != OutputState.OPEN) + throw new IllegalStateException("Response is " + _outputState); + + response.getHttpOutput().sendErrorClose(); + response.resetContent(); + request.getResponse().setStatus(code); + + request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + + _sendError = true; + if (_event != null) + { + Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + if (cause != null) + _event.addThrowable(cause); } } } @@ -997,29 +975,24 @@ protected void completed() if (LOG.isDebugEnabled()) LOG.debug("completed {}", toStringLocked()); - switch (_requestState) - { - case COMPLETING: - if (_event == null) - { - _requestState = RequestState.COMPLETED; - aListeners = null; - event = null; - if (_state == State.WAITING) - { - _state = State.WOKEN; - handle = true; - } - } - else - { - aListeners = _asyncListeners; - event = _event; - } - break; + if (_requestState != RequestState.COMPLETING) + throw new IllegalStateException(this.getStatusStringLocked()); - default: - throw new IllegalStateException(this.getStatusStringLocked()); + if (_event == null) + { + _requestState = RequestState.COMPLETED; + aListeners = null; + event = null; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } + else + { + aListeners = _asyncListeners; + event = _event; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 52549c9d153a..ba446af78c5b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -61,6 +61,7 @@ public class ErrorHandler extends AbstractHandler public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; boolean _showStacks = true; + boolean _disableStacks = false; boolean _showMessageInTitle = true; String _cacheControl = "must-revalidate,no-cache,no-store"; @@ -301,10 +302,10 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques if (LOG.isDebugEnabled()) LOG.warn(e); baseRequest.getResponse().resetContent(); - if (_showStacks) + if (!_disableStacks) { LOG.info("Disabling showsStacks for " + this); - _showStacks = false; + _disableStacks = true; continue; } break; @@ -355,7 +356,7 @@ protected void writeErrorPageBody(HttpServletRequest request, Writer writer, int String uri = request.getRequestURI(); writeErrorPageMessage(request, writer, code, message, uri); - if (showStacks) + if (showStacks && !_disableStacks) writeErrorPageStacks(request, writer); Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration() @@ -416,6 +417,8 @@ private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int while (cause != null) { writer.printf("CAUSED BY %s%n", cause); + if (_showStacks && !_disableStacks) + cause.printStackTrace(writer); cause = cause.getCause(); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java index 142f86726170..a14a6bcd09f4 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -184,6 +184,7 @@ public void testAsyncCompletion(Handler handler, int status, String message) thr // The write should happen but the callback is delayed HttpTester.Response response = HttpTester.parseResponse(client.getInputStream()); + assertThat(response, Matchers.notNullValue()); assertThat(response.getStatus(), is(status)); String content = response.getContent(); assertThat(content, containsString(message)); From e73522255aa154b31a1843ad9a21f875c49d0669 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Mon, 5 Aug 2019 22:14:29 +1000 Subject: [PATCH 50/57] Cleanup from Review renaming and some TODOs Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/server/HttpChannel.java | 15 +++++++++------ .../eclipse/jetty/server/HttpChannelState.java | 14 +++++++++----- .../java/org/eclipse/jetty/server/HttpOutput.java | 2 +- .../java/org/eclipse/jetty/server/Response.java | 3 +-- .../jetty/server/handler/ErrorHandler.java | 4 ++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index a29073aa673b..0cb118bac92b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -419,13 +419,13 @@ public boolean handle() { // Get ready to send an error response _request.setHandled(false); - _response.resetContent(); + _response.resetContent(); // TODO should we only do this here ??? _response.getHttpOutput().reopen(); // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; + int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; // TODO is this necessary still? _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); _response.setStatus(code); @@ -437,7 +437,7 @@ public boolean handle() errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - sendCompleteResponse(); + sendResponseAndComplete(); break; } @@ -481,19 +481,22 @@ public boolean handle() Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); if (failure == null) failure = x; - else + else if (x != failure) failure.addSuppressed(x); + // TODO do we really need to reset the code? Throwable cause = unwrap(failure, BadMessageException.class); int code = cause instanceof BadMessageException ? ((BadMessageException)cause).getCode() : 500; _response.setStatus(code); if (_state.isResponseCommitted()) + { abort(x); + } else { _response.resetContent(); - sendCompleteResponse(); + sendResponseAndComplete(); } } finally @@ -656,7 +659,7 @@ protected Throwable unwrap(Throwable failure, Class<?>... targets) return null; } - public void sendCompleteResponse() + public void sendResponseAndComplete() { try { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 38a6f1889965..16ed9946fd7f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -504,6 +504,7 @@ private Action nextAction(boolean handling) // so we will do a normal error dispatch _requestState = RequestState.BLOCKING; + /// TODO explain why we don't just call sendError here???? final Request request = _channel.getRequest(); ContextHandler.Context context = _event.getContext(); if (context != null) @@ -762,6 +763,8 @@ protected void thrownException(Throwable th) // + If the request is async, then any async listeners are give a chance to handle the exception in their onError handler. // + If the request is not async, or not handled by any async onError listener, then a normal sendError is done. + + // TODO make this a method? Runnable sendError = () -> { final Request request = _channel.getRequest(); @@ -798,6 +801,9 @@ else if (cause instanceof UnavailableException) request.setAttribute(ERROR_EXCEPTION, th); request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); sendError(code, message); + + // Ensure any async lifecycle is ended! + _requestState = RequestState.BLOCKING; }; final AsyncContextEvent asyncEvent; @@ -836,7 +842,6 @@ else if (cause instanceof UnavailableException) if (_asyncListeners == null || _asyncListeners.isEmpty()) { sendError.run(); - _requestState = RequestState.BLOCKING; return; } asyncEvent = _event; @@ -881,7 +886,6 @@ else if (cause instanceof UnavailableException) // The listeners did not invoke API methods // and the container must provide a default error dispatch. sendError.run(); - _requestState = RequestState.BLOCKING; return; case DISPATCH: @@ -927,9 +931,9 @@ public void sendError(int code, String message) if (_outputState != OutputState.OPEN) throw new IllegalStateException("Response is " + _outputState); - response.getHttpOutput().sendErrorClose(); - response.resetContent(); - request.getResponse().setStatus(code); + response.getHttpOutput().closedBySendError(); + response.resetContent(); // TODO do we need to do this here? + response.setStatus(code); request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index dddd5e0eded9..664dd96412da 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -264,7 +264,7 @@ private void abort(Throwable failure) _channel.abort(failure); } - public void sendErrorClose() + public void closedBySendError() { while (true) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 0a1ca984ccec..b896a10c074d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -407,7 +407,7 @@ public void sendError(int code, String message) throws IOException case -1: _channel.abort(new IOException(message)); break; - case 102: + case HttpStatus.PROCESSING_102: sendProcessing(); break; default: @@ -1085,7 +1085,6 @@ public void resetContent() i.remove(); continue; default: - continue; } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index ba446af78c5b..65eacba85ef5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -265,7 +265,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques // We will write it into a byte array buffer so // we can flush it asynchronously. - while(true) + while (true) { try { @@ -312,7 +312,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques } } - baseRequest.getHttpChannel().sendCompleteResponse(); + baseRequest.getHttpChannel().sendResponseAndComplete(); } protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) From 73ca4774c8bbef9ebd3adffe835d533a3ca2f006 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 6 Aug 2019 08:52:23 +1000 Subject: [PATCH 51/57] Cleanup from Review Checkstyle fixes Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/proxy/AbstractProxyServlet.java | 2 +- .../org/eclipse/jetty/server/handler/ErrorHandler.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index c670df04766a..3e6f53410bbe 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -684,7 +684,7 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ { proxyResponse.sendError(-1); } - catch(Exception e2) + catch (Exception e2) { _log.ignore(e2); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 65eacba85ef5..2d05b4fb9c93 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -380,7 +380,7 @@ protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, htmlRow(writer, "STATUS", status); htmlRow(writer, "MESSAGE", message); htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); - Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); while (cause != null) { htmlRow(writer, "CAUSED BY", cause); @@ -413,7 +413,7 @@ private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int writer.printf("STATUS: %s%n", code); writer.printf("MESSAGE: %s%n", message); writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); - Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); while (cause != null) { writer.printf("CAUSED BY %s%n", cause); @@ -431,9 +431,9 @@ private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int .append(" status: \"").append(Integer.toString(code)).append("\",\n") .append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n"); Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); - if (servlet !=null) + if (servlet != null) writer.append("servlet: \"").append(servlet.toString()).append("\",\n"); - Throwable cause = (Throwable) request.getAttribute(Dispatcher.ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); int c = 0; while (cause != null) { @@ -444,7 +444,6 @@ private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int writer.append("}"); } - protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { From 3873cd2015172b270d5ab67cfdc6ede50d42d21f Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 6 Aug 2019 11:51:04 +1000 Subject: [PATCH 52/57] Cleanup from Review Code cleanups and simplifications Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/server/HttpChannel.java | 138 ++++++----------- .../jetty/server/HttpChannelState.java | 146 +++++++----------- .../jetty/server/HttpConfiguration.java | 3 + .../jetty/server/handler/ContextHandler.java | 2 +- .../jetty/server/handler/ErrorHandler.java | 8 +- 5 files changed, 117 insertions(+), 180 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 0cb118bac92b..fb7763b2a782 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -32,6 +32,7 @@ import java.util.function.Supplier; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpFields; @@ -353,35 +354,17 @@ public boolean handle() _request.setHandled(false); _response.getHttpOutput().reopen(); - try + dispatch(DispatcherType.REQUEST, () -> { - _request.setDispatcherType(DispatcherType.REQUEST); - notifyBeforeDispatch(_request); - - List<HttpConfiguration.Customizer> customizers = _configuration.getCustomizers(); - if (!customizers.isEmpty()) + for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers()) { - for (HttpConfiguration.Customizer customizer : customizers) - { - customizer.customize(getConnector(), _configuration, _request); - if (_request.isHandled()) - break; - } + customizer.customize(getConnector(), _configuration, _request); + if (_request.isHandled()) + return; } + getServer().handle(HttpChannel.this); + }); - if (!_request.isHandled()) - getServer().handle(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } break; } @@ -390,22 +373,7 @@ public boolean handle() _request.setHandled(false); _response.getHttpOutput().reopen(); - try - { - _request.setDispatcherType(DispatcherType.ASYNC); - notifyBeforeDispatch(_request); - getServer().handleAsync(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } + dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this)); break; } @@ -419,32 +387,27 @@ public boolean handle() { // Get ready to send an error response _request.setHandled(false); - _response.resetContent(); // TODO should we only do this here ??? + _response.resetContent(); _response.getHttpOutput().reopen(); // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError - Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; // TODO is this necessary still? - _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - _response.setStatus(code); + Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + _response.setStatus(code != null ? code : HttpStatus.INTERNAL_SERVER_ERROR_500); ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); // If we can't have a body, then create a minimal error response. - if (HttpStatus.hasNoBody(code) || - errorHandler == null || - !errorHandler.errorPageForMethod(_request.getMethod())) + if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { sendResponseAndComplete(); break; } - // Look for an error page + // Look for an error page dispatcher String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; - if (errorDispatcher == null) { // Allow ErrorHandler to generate response @@ -454,45 +417,15 @@ public boolean handle() else { // Do the error page dispatch - try - { - _request.setAttribute(ErrorHandler.ERROR_PAGE, errorPage); - _request.setDispatcherType(DispatcherType.ERROR); - notifyBeforeDispatch(_request); - errorDispatcher.error(_request, _response); - break; - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } + dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response)); } } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not perform ERROR dispatch, aborting", x); - Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - if (failure == null) - failure = x; - else if (x != failure) - failure.addSuppressed(x); - - // TODO do we really need to reset the code? - Throwable cause = unwrap(failure, BadMessageException.class); - int code = cause instanceof BadMessageException ? ((BadMessageException)cause).getCode() : 500; - _response.setStatus(code); - if (_state.isResponseCommitted()) - { abort(x); - } else { _response.resetContent(); @@ -564,6 +497,11 @@ else if (x != failure) } } + // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED + // TODO Action triggered by the completed callback below. It would be possible to modify the + // TODO callback to do a non-blocking consumeAll at this point and only call completed when + // TODO that is done. + // Set a close callback on the HttpOutput to make it an async callback _response.getHttpOutput().setClosedCallback(Callback.from(_state::completed)); _response.closeOutput(); @@ -571,14 +509,11 @@ else if (x != failure) if (_response.getHttpOutput().getClosedCallback() != null) _response.getHttpOutput().getClosedCallback().succeeded(); - // TODO we could do an asynchronous consumeAll in the callback break; } default: - { throw new IllegalStateException(this.toString()); - } } } catch (Throwable failure) @@ -599,6 +534,26 @@ else if (x != failure) return !suspended; } + private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException + { + try + { + _request.setDispatcherType(type); + notifyBeforeDispatch(_request); + dispatchable.dispatch(); + } + catch (Throwable x) + { + notifyDispatchFailure(_request, x); + throw x; + } + finally + { + notifyAfterDispatch(_request); + _request.setDispatcherType(null); + } + } + /** * <p>Sends an error 500, performing a special logic to detect whether the request is suspended, * to avoid concurrent writes from the application.</p> @@ -635,7 +590,7 @@ else if (noStack != null) if (isCommitted()) abort(failure); else - _state.thrownException(failure); + _state.onError(failure); } /** @@ -784,7 +739,7 @@ public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); String reason = failure.getReason(); - if (status < 400 || status > 599) + if (status < HttpStatus.BAD_REQUEST_400 || status > 599) failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); notifyRequestFailure(_request, failure); @@ -855,7 +810,9 @@ public boolean sendResponse(MetaData.Response info, ByteBuffer content, boolean // wrap callback to process 100 responses final int status = info.getStatus(); - final Callback committed = (status < 200 && status >= 100) ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); + final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) + ? new Send100Callback(callback) + : new SendCallback(callback, content, true, complete); notifyResponseBegin(_request); @@ -1109,6 +1066,11 @@ private void notifyEvent2(Function<Listener, BiConsumer<Request, Throwable>> fun } } + interface Dispatchable + { + void dispatch() throws IOException, ServletException; + } + /** * <p>Listener for {@link HttpChannel} events.</p> * <p>HttpChannel will emit events for the various phases it goes through while diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 16ed9946fd7f..d75eb7a7e5b2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -499,19 +499,10 @@ private Action nextAction(boolean handling) case EXPIRING: if (handling) throw new IllegalStateException(getStatusStringLocked()); - - // We must have already called onTimeout and nothing changed, - // so we will do a normal error dispatch + sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "AsyncContext timeout"); + // handle sendError immediately _requestState = RequestState.BLOCKING; - - /// TODO explain why we don't just call sendError here???? - final Request request = _channel.getRequest(); - ContextHandler.Context context = _event.getContext(); - if (context != null) - request.setAttribute(ErrorHandler.ERROR_CONTEXT, context); - request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(ERROR_STATUS_CODE, 500); - request.setAttribute(ERROR_MESSAGE, "AsyncContext timeout"); + _sendError = false; return Action.SEND_ERROR; case COMPLETE: @@ -757,55 +748,8 @@ public void asyncError(Throwable failure) } } - protected void thrownException(Throwable th) + protected void onError(Throwable th) { - // This method is called by HttpChannel.handleException to handle an exception thrown from a dispatch: - // + If the request is async, then any async listeners are give a chance to handle the exception in their onError handler. - // + If the request is not async, or not handled by any async onError listener, then a normal sendError is done. - - - // TODO make this a method? - Runnable sendError = () -> - { - final Request request = _channel.getRequest(); - - // Determine the actual details of the exception - final int code; - final String message; - Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - if (cause == null) - { - code = HttpStatus.INTERNAL_SERVER_ERROR_500; - message = th.toString(); - } - else if (cause instanceof BadMessageException) - { - BadMessageException bme = (BadMessageException)cause; - code = bme.getCode(); - message = bme.getReason(); - } - else if (cause instanceof UnavailableException) - { - message = cause.toString(); - if (((UnavailableException)cause).isPermanent()) - code = HttpStatus.NOT_FOUND_404; - else - code = HttpStatus.SERVICE_UNAVAILABLE_503; - } - else - { - code = HttpStatus.INTERNAL_SERVER_ERROR_500; - message = null; - } - - request.setAttribute(ERROR_EXCEPTION, th); - request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); - sendError(code, message); - - // Ensure any async lifecycle is ended! - _requestState = RequestState.BLOCKING; - }; - final AsyncContextEvent asyncEvent; final List<AsyncListener> asyncListeners; synchronized (this) @@ -817,6 +761,7 @@ else if (cause instanceof UnavailableException) if (_state != State.HANDLING) throw new IllegalStateException(getStatusStringLocked()); + // If sendError has already been called, we can only handle one failure at a time! if (_sendError) { LOG.warn("unhandled due to prior sendError", th); @@ -828,20 +773,15 @@ else if (cause instanceof UnavailableException) { case BLOCKING: // handle the exception with a sendError - sendError.run(); + sendError(th); return; - case DISPATCH: - case COMPLETE: - // Complete or Dispatch have been called, but the original subsequently threw an exception. - // TODO // GW I think we really should ignore, but will fall through for now. - // TODO LOG.warn("unhandled due to prior dispatch/complete", th); - // TODO return; - + case DISPATCH: // Dispatch has already been called but we ignore and handle exception below + case COMPLETE: // Complete has already been called but we ignore and handle exception below case ASYNC: if (_asyncListeners == null || _asyncListeners.isEmpty()) { - sendError.run(); + sendError(th); return; } asyncEvent = _event; @@ -876,28 +816,57 @@ else if (cause instanceof UnavailableException) // check the actions of the listeners synchronized (this) { - // if anybody has called sendError then we've handled as much as we can by calling listeners - if (_sendError) - return; - - switch (_requestState) - { - case ASYNC: - // The listeners did not invoke API methods - // and the container must provide a default error dispatch. - sendError.run(); - return; + // If we are still async and nobody has called sendError + if (_requestState == RequestState.ASYNC && !_sendError) + // Then the listeners did not invoke API methods + // and the container must provide a default error dispatch. + sendError(th); + else + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + } + } - case DISPATCH: - case COMPLETE: - // The listeners handled the exception by calling dispatch() or complete(). - return; + private void sendError(Throwable th) + { + // No sync as this is always called with lock held - default: - LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); - return; - } + // Determine the actual details of the exception + final Request request = _channel.getRequest(); + final int code; + final String message; + Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); + if (cause == null) + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = th.toString(); } + else if (cause instanceof BadMessageException) + { + BadMessageException bme = (BadMessageException)cause; + code = bme.getCode(); + message = bme.getReason(); + } + else if (cause instanceof UnavailableException) + { + message = cause.toString(); + if (((UnavailableException)cause).isPermanent()) + code = HttpStatus.NOT_FOUND_404; + else + code = HttpStatus.SERVICE_UNAVAILABLE_503; + } + else + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = null; + } + + sendError(code, message); + + // No ISE, so good to modify request/state + request.setAttribute(ERROR_EXCEPTION, th); + request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); + // Ensure any async lifecycle is ended! + _requestState = RequestState.BLOCKING; } public void sendError(int code, String message) @@ -932,7 +901,6 @@ public void sendError(int code, String message) throw new IllegalStateException("Response is " + _outputState); response.getHttpOutput().closedBySendError(); - response.resetContent(); // TODO do we need to do this here? response.setStatus(code); request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 4de4848708c1..c1b385babe67 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -158,6 +159,8 @@ public void addCustomizer(Customizer customizer) public List<Customizer> getCustomizers() { + if (_customizers.isEmpty()) + return Collections.emptyList(); return _customizers; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index afec04af9d79..80f43b2d3371 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1103,7 +1103,7 @@ public boolean checkContext(final String target, final Request baseRequest, fina case UNAVAILABLE: baseRequest.setHandled(true); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return true; + return false; default: if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) return false; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index 2d05b4fb9c93..14700779b34e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -263,12 +263,15 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques return; } - // We will write it into a byte array buffer so - // we can flush it asynchronously. + // write into the response aggregate buffer and flush it asynchronously. while (true) { try { + // TODO currently the writer used here is of fixed size, so a large + // TODO error page may cause a BufferOverflow. In which case we try + // TODO again with stacks disabled. If it still overflows, it is + // TODO written without a body. ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer(); ByteBufferOutputStream out = new ByteBufferOutputStream(buffer); PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset)); @@ -312,6 +315,7 @@ protected void generateAcceptableResponse(Request baseRequest, HttpServletReques } } + // Do an asynchronous completion. baseRequest.getHttpChannel().sendResponseAndComplete(); } From 0d7cd741cae8580f5a422a60d4a38aa840a88a14 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Wed, 7 Aug 2019 12:03:15 +1000 Subject: [PATCH 53/57] fixed debug Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../src/main/java/org/eclipse/jetty/server/HttpChannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index fb7763b2a782..d2647d3a0d9c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -716,7 +716,7 @@ public boolean onRequestComplete() public void onCompleted() { if (LOG.isDebugEnabled()) - LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten()); + LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten()); if (_requestLog != null) _requestLog.log(_request, _response); From aaaa71ef21f9ab0251b969bf90e4c7264f792f53 Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Wed, 7 Aug 2019 16:37:06 +1000 Subject: [PATCH 54/57] Cleanup from Review Ensure response sent before server shutdown Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../java/org/eclipse/jetty/server/handler/ShutdownHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 24a1258b61d0..ed5affa51c9e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -197,8 +197,9 @@ protected void doShutdown(Request baseRequest, HttpServletResponse response) thr connector.shutdown(); } - response.sendError(200, "Connectors closed, commencing full shutdown"); baseRequest.setHandled(true); + response.setStatus(200); + response.flushBuffer(); final Server server = getServer(); new Thread() From da5eb1ea5e22f9fdd744efe57112c71ea47f2d6f Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Tue, 13 Aug 2019 10:13:14 +1000 Subject: [PATCH 55/57] removed unnecessary optimisation Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../main/java/org/eclipse/jetty/server/HttpConfiguration.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index e68c1e0f5093..9aa9b62fcc86 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.server; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -160,8 +159,6 @@ public void addCustomizer(Customizer customizer) public List<Customizer> getCustomizers() { - if (_customizers.isEmpty()) - return Collections.emptyList(); return _customizers; } From 98f629646604352f962643759721fb20148f999f Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Wed, 21 Aug 2019 12:10:14 +1000 Subject: [PATCH 56/57] fixed duplicate from merge Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../jetty/websocket/tests/WebSocketConnectionStatsTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java index 812a75cc0a77..148720950c40 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketConnectionStatsTest.java @@ -123,7 +123,6 @@ long getFrameByteSize(WebSocketFrame frame) @Disabled("Flaky test see issue #3982") @Test - @Disabled // TODO this is a flakey test public void echoStatsTest() throws Exception { URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/testPath"); @@ -177,4 +176,4 @@ public void echoStatsTest() throws Exception assertThat("stats.sendBytes", statistics.getSentBytes(), is(expectedSent)); assertThat("stats.receivedBytes", statistics.getReceivedBytes(), is(expectedReceived)); } -} \ No newline at end of file +} From c62add8ae7a15e23cc71b69ae41cb9023c9aba1e Mon Sep 17 00:00:00 2001 From: Greg Wilkins <gregw@webtide.com> Date: Thu, 22 Aug 2019 18:29:51 +1000 Subject: [PATCH 57/57] Updates from review Signed-off-by: Greg Wilkins <gregw@webtide.com> --- .../org/eclipse/jetty/server/HttpChannel.java | 6 +-- .../org/eclipse/jetty/server/HttpOutput.java | 45 ++++++++++++------- .../org/eclipse/jetty/server/Response.java | 7 +++ .../jetty/util/thread/QueuedThreadPool.java | 8 +--- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index fbeebae57c7f..31714973255f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -503,11 +503,7 @@ public boolean handle() // TODO that is done. // Set a close callback on the HttpOutput to make it an async callback - _response.getHttpOutput().setClosedCallback(Callback.from(_state::completed)); - _response.closeOutput(); - // ensure the callback actually got called - if (_response.getHttpOutput().getClosedCallback() != null) - _response.getHttpOutput().getClosedCallback().succeeded(); + _response.closeOutput(Callback.from(_state::completed)); break; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 664dd96412da..655e8cd5dbdb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -284,27 +285,36 @@ public void closedBySendError() } } - /** - * Make {@link #close()} am asynchronous method - * @param closeCallback The callback to use when close() is called. - */ - public void setClosedCallback(Callback closeCallback) + public void close(Closeable wrapper, Callback callback) { - _closeCallback = closeCallback; - } - - public Callback getClosedCallback() - { - return _closeCallback; + _closeCallback = callback; + try + { + if (wrapper != null) + wrapper.close(); + if (!isClosed()) + close(); + } + catch (Throwable th) + { + closed(); + if (_closeCallback == null) + LOG.ignore(th); + else + callback.failed(th); + } + finally + { + if (_closeCallback != null) + callback.succeeded(); + _closeCallback = null; + } } @Override public void close() { - Callback closeCallback = _closeCallback; - _closeCallback = null; - if (closeCallback == null) - closeCallback = BLOCKING_CLOSE_CALLBACK; + Callback closeCallback = _closeCallback == null ? BLOCKING_CLOSE_CALLBACK : _closeCallback; while (true) { @@ -314,6 +324,7 @@ public void close() case CLOSING: case CLOSED: { + _closeCallback = null; closeCallback.succeeded(); return; } @@ -342,6 +353,7 @@ public void close() LOG.warn(ex.toString()); LOG.debug(ex); abort(ex); + _closeCallback = null; closeCallback.failed(ex); return; } @@ -359,16 +371,19 @@ public void close() { // Do a blocking close write(content, !_channel.getResponse().isIncluding()); + _closeCallback = null; closeCallback.succeeded(); } else { + _closeCallback = null; write(content, !_channel.getResponse().isIncluding(), closeCallback); } } catch (IOException x) { LOG.ignore(x); // Ignore it, it's been already logged in write(). + _closeCallback = null; closeCallback.failed(x); } return; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index f58d7e77ab00..04870c509492 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.PrintWriter; import java.nio.channels.IllegalSelectorException; @@ -57,6 +58,7 @@ import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; @@ -801,6 +803,11 @@ public void closeOutput() throws IOException _out.close(); } + public void closeOutput(Callback callback) + { + _out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback); + } + public long getLongContentLength() { return _contentLength; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index 3f5addd27bba..41f02b80e8bf 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -171,10 +171,11 @@ protected void doStop() throws Exception if (LOG.isDebugEnabled()) LOG.debug("Stopping {}", this); + super.doStop(); + removeBean(_tryExecutor); _tryExecutor = TryExecutor.NO_TRY; - super.doStop(); // Signal the Runner threads that we are stopping int threads = _counts.getAndSetHi(Integer.MIN_VALUE); @@ -184,11 +185,6 @@ protected void doStop() throws Exception BlockingQueue<Runnable> jobs = getQueue(); if (timeout > 0) { - // Consume any reserved threads - while (tryExecute(NOOP)) - { - } - // Fill the job queue with noop jobs to wakeup idle threads. for (int i = 0; i < threads; ++i) {