From 37d0b7c58c161893f568c950ddab2b37d8272034 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Wed, 18 Dec 2024 16:00:20 +1100 Subject: [PATCH] Issue #12505 - Server to Servlet Error Handling (#12586) * Issue #12505 server to servlet api error handling --------- Co-authored-by: gregw --- .../jetty/ee10/servlet/Dispatcher.java | 54 ++++-- .../jetty/ee10/servlet/ErrorHandler.java | 17 +- .../ee10/servlet/ErrorPageErrorHandler.java | 7 +- .../jetty/ee10/servlet/ResourceServlet.java | 2 +- .../jetty/ee10/servlet/ServletChannel.java | 4 +- .../ee10/servlet/ServletChannelState.java | 21 ++- .../ee10/servlet/ServletContextHandler.java | 14 +- .../jetty/ee10/servlet/ErrorPageTest.java | 77 ++++++++- .../jetty/ee10/webapp/WebAppContextTest.java | 154 +++++++++++++++++ .../jetty/ee11/servlet/ErrorHandler.java | 71 ++++---- .../ee11/servlet/ErrorPageErrorHandler.java | 132 ++++----------- .../ee11/servlet/ServletApiResponse.java | 4 +- .../ee11/servlet/ServletChannelState.java | 17 +- .../ee11/servlet/ServletContextHandler.java | 21 ++- .../jetty/ee11/servlet/ErrorPageTest.java | 72 ++++++++ .../jetty/ee11/webapp/WebAppContextTest.java | 155 ++++++++++++++++++ .../jetty/ee9/nested/ContextHandler.java | 7 + .../jetty/ee9/webapp/WebAppContextTest.java | 145 ++++++++++++++++ 18 files changed, 802 insertions(+), 172 deletions(-) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java index a923e3f9ac83..ccdd8d851222 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.io.WriterOutputStream; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; @@ -58,10 +59,10 @@ public class Dispatcher implements RequestDispatcher * Dispatch include attribute names */ public static final String __FORWARD_PREFIX = "jakarta.servlet.forward."; - + /** * Name of original request attribute - */ + */ public static final String __ORIGINAL_REQUEST = "org.eclipse.jetty.originalRequest"; public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; @@ -316,7 +317,7 @@ public String getRequestURI() @Override public StringBuffer getRequestURL() { - return _uri == null ? super.getRequestURL() : new StringBuffer(HttpURI.build(_uri).query(null).scheme(super.getScheme()).host(super.getServerName()).port(super.getServerPort()).asString()); + return _uri == null ? super.getRequestURL() : new StringBuffer(HttpURI.build(_uri).query(null).scheme(super.getScheme()).host(super.getServerName()).port(super.getServerPort()).asString()); } @Override @@ -324,7 +325,7 @@ public Object getAttribute(String name) { if (name == null) return null; - + //Servlet Spec 9.4.2 no forward attributes if a named dispatcher if (_named != null && name.startsWith(__FORWARD_PREFIX)) return null; @@ -356,7 +357,9 @@ public Object getAttribute(String name) return originalRequest == null ? _httpServletRequest : originalRequest; } // Forward should hide include. - case RequestDispatcher.INCLUDE_MAPPING, RequestDispatcher.INCLUDE_SERVLET_PATH, RequestDispatcher.INCLUDE_PATH_INFO, RequestDispatcher.INCLUDE_REQUEST_URI, RequestDispatcher.INCLUDE_CONTEXT_PATH, RequestDispatcher.INCLUDE_QUERY_STRING -> + case RequestDispatcher.INCLUDE_MAPPING, RequestDispatcher.INCLUDE_SERVLET_PATH, + RequestDispatcher.INCLUDE_PATH_INFO, RequestDispatcher.INCLUDE_REQUEST_URI, + RequestDispatcher.INCLUDE_CONTEXT_PATH, RequestDispatcher.INCLUDE_QUERY_STRING -> { return null; } @@ -380,11 +383,11 @@ public Object getAttribute(String name) public Enumeration getAttributeNames() { ArrayList names = new ArrayList<>(Collections.list(super.getAttributeNames())); - + //Servlet Spec 9.4.2 no forward attributes if a named dispatcher if (_named != null) return Collections.enumeration(names); - + names.add(RequestDispatcher.FORWARD_REQUEST_URI); names.add(RequestDispatcher.FORWARD_SERVLET_PATH); names.add(RequestDispatcher.FORWARD_PATH_INFO); @@ -416,7 +419,7 @@ public Object getAttribute(String name) { if (name == null) return null; - + //Servlet Spec 9.3.1 no include attributes if a named dispatcher if (_named != null && name.startsWith(__INCLUDE_PREFIX)) return null; @@ -440,7 +443,7 @@ public Enumeration getAttributeNames() ArrayList names = new ArrayList<>(Collections.list(super.getAttributeNames())); if (_named != null) return Collections.enumeration(names); - + names.add(RequestDispatcher.INCLUDE_MAPPING); names.add(RequestDispatcher.INCLUDE_SERVLET_PATH); names.add(RequestDispatcher.INCLUDE_PATH_INFO); @@ -462,7 +465,7 @@ private static class IncludeResponse extends HttpServletResponseWrapper ServletOutputStream _servletOutputStream; PrintWriter _printWriter; PrintWriter _mustFlush; - + public IncludeResponse(HttpServletResponse response) { super(response); @@ -753,9 +756,12 @@ public Enumeration getAttributeNames() private class ErrorRequest extends ParameterRequestWrapper { + private final HttpServletRequest _httpServletRequest; + public ErrorRequest(HttpServletRequest httpRequest) { super(httpRequest); + _httpServletRequest = httpRequest; } @Override @@ -797,6 +803,34 @@ public StringBuffer getRequestURL() .port(getServerPort()) .asString()); } + + @Override + public Object getAttribute(String name) + { + return switch (name) + { + case ERROR_REQUEST_URI -> _httpServletRequest.getRequestURI(); + case ERROR_STATUS_CODE -> super.getAttribute(ErrorHandler.ERROR_STATUS); + case ERROR_MESSAGE -> super.getAttribute(ErrorHandler.ERROR_MESSAGE); + case ERROR_SERVLET_NAME -> super.getAttribute(ErrorHandler.ERROR_ORIGIN); + case ERROR_EXCEPTION -> super.getAttribute(ErrorHandler.ERROR_EXCEPTION); + case ERROR_EXCEPTION_TYPE -> + { + Object err = super.getAttribute(ErrorHandler.ERROR_EXCEPTION); + yield err == null ? null : err.getClass(); + } + default -> super.getAttribute(name); + }; + } + + @Override + public Enumeration getAttributeNames() + { + // TODO add all names? + List names = new ArrayList<>(List.of(ERROR_REQUEST_URI, ERROR_STATUS_CODE, ERROR_MESSAGE, ERROR_SERVLET_NAME, ERROR_EXCEPTION, ERROR_EXCEPTION_TYPE)); + names.addAll(Collections.list(super.getAttributeNames())); + return Collections.enumeration(names); + } } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java index e1ab345f5fff..ecc88d6e0a7b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java @@ -27,7 +27,6 @@ import java.util.Map; import java.util.stream.Collectors; -import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -128,7 +127,7 @@ public boolean handle(Request request, Response response, Callback callback) thr } } - String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE); + String message = (String)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_MESSAGE); if (message == null) message = HttpStatus.getMessage(response.getStatus()); generateAcceptableResponse(servletContextRequest, httpServletRequest, httpServletResponse, response.getStatus(), message); @@ -416,9 +415,9 @@ protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, htmlRow(writer, "MESSAGE", message); if (isShowServlet()) { - htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + htmlRow(writer, "SERVLET", request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_ORIGIN)); } - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION); while (cause != null) { htmlRow(writer, "CAUSED BY", cause); @@ -451,9 +450,9 @@ protected void writeErrorPlain(HttpServletRequest request, PrintWriter writer, i writer.printf("MESSAGE: %s%n", message); if (isShowServlet()) { - writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.printf("SERVLET: %s%n", request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_ORIGIN)); } - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION); while (cause != null) { writer.printf("CAUSED BY %s%n", cause); @@ -467,8 +466,8 @@ protected void writeErrorPlain(HttpServletRequest request, PrintWriter writer, i protected void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message) { - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); + Throwable cause = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION); + Object servlet = request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_ORIGIN); Map json = new HashMap<>(); json.put("url", request.getRequestURI()); @@ -492,7 +491,7 @@ protected void writeErrorJson(HttpServletRequest request, PrintWriter writer, in protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { - Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + Throwable th = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION); if (th != null) { writer.write("

Caused by:

");
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorPageErrorHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorPageErrorHandler.java
index 6dda7a4e7720..5f3892e432fd 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorPageErrorHandler.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorPageErrorHandler.java
@@ -65,7 +65,7 @@ public String getErrorPage(HttpServletRequest request)
         PageLookupTechnique pageSource = null;
 
         Class matchedThrowable = null;
-        Throwable error = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
+        Throwable error = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION);
         Throwable cause = error;
 
         // Walk the cause hierarchy
@@ -98,6 +98,7 @@ public String getErrorPage(HttpServletRequest request)
             {
                 request.setAttribute(Dispatcher.ERROR_EXCEPTION, unwrapped);
                 request.setAttribute(Dispatcher.ERROR_EXCEPTION_TYPE, unwrapped.getClass());
+                request.setAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION, unwrapped);
             }
         }
 
@@ -108,7 +109,7 @@ public String getErrorPage(HttpServletRequest request)
             pageSource = PageLookupTechnique.STATUS_CODE;
 
             // look for an exact code match
-            errorStatusCode = (Integer)request.getAttribute(Dispatcher.ERROR_STATUS_CODE);
+            errorStatusCode = (Integer)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS);
             if (errorStatusCode != null)
             {
                 errorPage = _errorPages.get(Integer.toString(errorStatusCode));
@@ -149,7 +150,7 @@ public String getErrorPage(HttpServletRequest request)
                     dbg.append(" (using matched Throwable ");
                     dbg.append(matchedThrowable.getName());
                     dbg.append(" / actually thrown as ");
-                    Throwable originalThrowable = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
+                    Throwable originalThrowable = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION);
                     dbg.append(originalThrowable.getClass().getName());
                     dbg.append(')');
                     LOG.debug(dbg.toString(), cause);
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java
index 2146974dff42..59b6c71e3faf 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java
@@ -780,7 +780,7 @@ protected void writeHttpError(Request coreRequest, Response coreResponse, Callba
                 if (isIncluded(request))
                     return;
                 if (cause != null)
-                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
+                    request.setAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION, cause);
                 response.sendError(statusCode, reason);
             }
             catch (IOException e)
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java
index 8a81323cfb6c..b0018a649654 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java
@@ -461,7 +461,7 @@ public void handle()
 
                             // the following is needed as you cannot trust the response code and reason
                             // as those could have been modified after calling sendError
-                            Integer code = (Integer)_servletContextRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+                            Integer code = (Integer)_servletContextRequest.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS);
                             if (code == null)
                                 code = HttpStatus.INTERNAL_SERVER_ERROR_500;
                             getServletContextResponse().setStatus(code);
@@ -474,7 +474,7 @@ public void handle()
                             if (!_httpInput.consumeAvailable())
                                 ResponseUtils.ensureNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse());
 
-                            ContextHandler.ScopedContext context = (ContextHandler.ScopedContext)_servletContextRequest.getAttribute(ErrorHandler.ERROR_CONTEXT);
+                            ContextHandler.ScopedContext context = (ContextHandler.ScopedContext)_servletContextRequest.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_CONTEXT);
                             Request.Handler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler());
 
                             // If we can't have a body or have no ErrorHandler, then create a minimal error response.
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java
index 3c121ba78039..9d007939b125 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java
@@ -40,6 +40,7 @@
 import static jakarta.servlet.RequestDispatcher.ERROR_REQUEST_URI;
 import static jakarta.servlet.RequestDispatcher.ERROR_SERVLET_NAME;
 import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE;
+import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS;
 
 /**
  * holder of the state of request-response cycle.
@@ -423,6 +424,16 @@ public Action handling()
                         throw new IllegalStateException(getStatusStringLocked());
                     _initial = true;
                     _state = State.HANDLING;
+                    if (_servletChannel.getResponse().getStatus() != 0)
+                    {
+                        if (_servletChannel.getRequest().getAttribute(ERROR_STATUS) instanceof Integer errorCode)
+                        {
+                            _servletChannel.getServletRequestState().sendError(errorCode, null);
+                            _requestState = RequestState.BLOCKING;
+                            _sendError = false;
+                            return Action.SEND_ERROR;
+                        }
+                    }
                     return Action.DISPATCH;
 
                 case WOKEN:
@@ -1022,7 +1033,6 @@ else if (cause instanceof UnavailableException)
 
     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:
@@ -1058,21 +1068,18 @@ public void sendError(int code, String message)
             response.setStatus(code);
             servletContextRequest.errorClose();
 
-            request.setAttribute(org.eclipse.jetty.ee10.servlet.ErrorHandler.ERROR_CONTEXT, servletContextRequest.getErrorContext());
-            request.setAttribute(ERROR_REQUEST_URI, httpServletRequest.getRequestURI());
-            request.setAttribute(ERROR_SERVLET_NAME, servletContextRequest.getServletName());
-            request.setAttribute(ERROR_STATUS_CODE, code);
-            request.setAttribute(ERROR_MESSAGE, message);
-
             // Set Jetty Specific Attributes.
             request.setAttribute(ErrorHandler.ERROR_CONTEXT, servletContextRequest.getServletContext());
             request.setAttribute(ErrorHandler.ERROR_MESSAGE, message);
             request.setAttribute(ErrorHandler.ERROR_STATUS, code);
+            request.setAttribute(ErrorHandler.ERROR_ORIGIN, servletContextRequest.getServletName());
 
             _sendError = true;
             if (_event != null)
             {
                 Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION);
+                if (cause == null)
+                    cause = (Throwable)request.getAttribute(ErrorHandler.ERROR_EXCEPTION);
                 if (cause != null)
                     _event.addThrowable(cause);
             }
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java
index 6e152a972dd9..42e4d2994357 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java
@@ -73,6 +73,7 @@
 import org.eclipse.jetty.ee10.servlet.security.ConstraintAware;
 import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
 import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.http.HttpURI;
 import org.eclipse.jetty.http.pathmap.MatchedResource;
 import org.eclipse.jetty.io.IOResources;
@@ -1147,7 +1148,6 @@ protected ContextRequest wrapRequest(Request request, Response response)
         decodedPathInContext = URIUtil.decodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath()));
         matchedResource = _servletHandler.getMatchedServlet(decodedPathInContext);
 
-
         if (matchedResource == null)
             return wrapNoServlet(request, response);
         ServletHandler.MappedServlet mappedServlet = matchedResource.getResource();
@@ -1196,7 +1196,17 @@ protected boolean handleByContextHandler(String pathInContext, ContextRequest re
         boolean initialDispatch = request instanceof ServletContextRequest;
         if (!initialDispatch)
             return false;
-        return super.handleByContextHandler(pathInContext, request, response, callback);
+
+        if (isProtectedTarget(pathInContext))
+        {
+            // At this point we have not entered the state machine of the ServletChannelState, so we do nothing here
+            // other than to set the error status attribute (and also status). When execution proceeds normally into the
+            // state machine the request will be treated as an error. Note that we set both the error status and request
+            // status because the request status is cheaper to check than the error status attribute.
+            request.setAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS, 404);
+            response.setStatus(HttpStatus.NOT_FOUND_404);
+        }
+        return false;
     }
 
     @Override
diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java
index 3667b60f8dde..894b6c6c4ad9 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java
@@ -74,6 +74,7 @@
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -1444,7 +1445,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
             }
         };
 
-        contextHandler.addServlet(asyncServlet, "/async/*");
+        contextHandler.addServlet(asyncServlet, "/async/*").setAsyncSupported(true);
         contextHandler.addServlet(ErrorDumpServlet.class, "/error/*");
 
         ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler();
@@ -1862,8 +1863,9 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
         HttpServlet error598Servlet = new HttpServlet()
         {
             @Override
-            protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
+            protected void service(HttpServletRequest req, HttpServletResponse resp)
             {
+                assertThat(req.getDispatcherType(), is(DispatcherType.ERROR));
                 AsyncContext asyncContext = req.startAsync();
                 asyncContext.start(() ->
                 {
@@ -1950,6 +1952,68 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
         assertThat(response.getStatus(), is(598));
     }
 
+    @Test
+    public void testProtectedTargetNoPage() throws Exception
+    {
+        ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
+        contextHandler.setContextPath("/ctx");
+        contextHandler.setProtectedTargets(new String[] {"/WEB-INF", "/META-INF"});
+        contextHandler.addServlet(ErrorDumpServlet.class, "/error/*");
+        contextHandler.addServlet(new OkServlet(), "/*");
+
+        ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler();
+        contextHandler.setErrorHandler(errorPageErrorHandler);
+
+        startServer(contextHandler);
+
+        String rawRequest = """
+            GET /ctx/WEB-INF/anything HTTP/1.1\r
+            Host: test\r
+            Connection: close\r
+            Accept: */*\r
+            Accept-Charset: *\r
+            \r
+            """;
+
+        String rawResponse = _connector.getResponse(rawRequest);
+        assertThat(rawResponse, startsWith("HTTP/1.1 404 Not Found"));
+        HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+        assertThat(response.getStatus(), is(404));
+        assertThat(response.getContent(), containsString("

HTTP ERROR 404 Not Found

")); + } + + @Test + public void testProtectedTarget() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/ctx"); + contextHandler.setProtectedTargets(new String[] {"/WEB-INF", "/META-INF"}); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(new OkServlet(), "/*"); + + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + contextHandler.setErrorHandler(errorPageErrorHandler); + errorPageErrorHandler.addErrorPage(404, "/error/404"); + + startServer(contextHandler); + + String rawRequest = """ + GET /ctx/WEB-INF/anything HTTP/1.1\r + Host: test\r + Connection: close\r + Accept: */*\r + Accept-Charset: *\r + \r + """; + + String rawResponse = _connector.getResponse(rawRequest); + assertThat(rawResponse, startsWith("HTTP/1.1 404 Not Found")); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getContent(), containsString("ERROR_PAGE: /404")); + assertThat(response.getContent(), containsString("ERROR_MESSAGE: Not Found")); + } + public static class ErrorDumpServlet extends HttpServlet { @Override @@ -2067,4 +2131,13 @@ public TestServletException(Throwable rootCause) super(rootCause); } } + + public static class OkServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + resp.setStatus(200); + } + } } diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java index 3a7b38673c9a..9455f176b062 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java @@ -15,6 +15,7 @@ import java.io.File; import java.io.IOException; +import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.nio.file.FileSystem; @@ -32,16 +33,26 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.DispatcherType; import jakarta.servlet.GenericServlet; import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee.WebAppClassLoading; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.Dispatcher; import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee10.servlet.ServletChannel; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; @@ -84,6 +95,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -153,6 +165,52 @@ private Path createWar(Path tempDir, String name) throws Exception return warFile; } + @Test + public void testProtectedTargetErrorPage() throws Exception + { + WebAppContext contextHandler = new WebAppContext(); + contextHandler.setContextPath("/foo"); + contextHandler.setBaseResourceAsPath(Path.of("/tmp")); + ServletHolder defaultHolder = new ServletHolder(new DefaultServlet()); + defaultHolder.setDisplayName("default"); + + contextHandler.addServlet(defaultHolder, "/"); + contextHandler.addServlet(new OkServlet(), "/*"); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(GlobalErrorDumpServlet.class, "/global/*"); + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + errorPageErrorHandler.addErrorPage(404, "/error/TestException"); + errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/global/TestException"); + contextHandler.setErrorHandler(errorPageErrorHandler); + Server server = new Server(); + server.setHandler(contextHandler); + + LocalConnector connector = new LocalConnector(server); + server.addConnector(connector); + server.start(); + + try (StacklessLogging stackless = new StacklessLogging(ServletChannel.class)) + { + String rawRequest = """ + GET /foo/WEB-INF/classes/this/does/not/exist HTTP/1.1\r + Host: test\r + Connection: close\r + \r + """; + + String rawResponse = connector.getResponse(rawRequest); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getValuesList("ERRORDUMPSERVLET"), contains("ERRORDUMPSERVLET")); + String content = response.getContent(); + assertThat(content, containsString("ERROR_REQUEST_URI: /foo/WEB-INF/classes/this/does/not/exist")); + assertThat(content, containsString("getRequestURI()=[/foo/error/TestException]")); + assertThat(content, containsString("DISPATCH: ERROR")); + assertThat(content, not(containsString("GLOBALERRORDUMPSERVLET"))); + } + } + @Test public void testDefaultContextPath() throws Exception { @@ -518,6 +576,93 @@ public void service(ServletRequest req, ServletResponse res) } } + public static class ErrorDumpServlet extends HttpServlet + { + @Override + 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()); + + response.setHeader("ERRORDUMPSERVLET", "ERRORDUMPSERVLET"); + PrintWriter writer = response.getWriter(); + writer.println("DISPATCH: " + request.getDispatcherType().name()); + writer.println("ERROR_PAGE: " + request.getPathInfo()); + writer.println("ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + + protected String valueOf(Object obj) + { + if (obj == null) + return "null"; + return valueOf(obj.toString()); + } + + protected String valueOf(String str) + { + if (str == null) + return "null"; + return String.format("[%s]", str); + } + } + + public static class GlobalErrorDumpServlet extends ErrorDumpServlet + { + @Override + 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()); + + response.setHeader("GLOBALERRORDUMPSERVLET", "GLOBALERRORDUMPSERVLET"); + + PrintWriter writer = response.getWriter(); + writer.println("GLOBAL DISPATCH: " + request.getDispatcherType().name()); + writer.println("GLOBAL ERROR_PAGE: " + request.getPathInfo()); + writer.println("GLOBAL ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("GLOBAL ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("GLOBAL ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("GLOBAL ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("GLOBAL ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("GLOBAL ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + } + @Test public void testBaseResourceAbsolutePath(WorkDir workDir) throws Exception { @@ -1014,4 +1159,13 @@ public void testAddProtectedClasses() throws Exception assertThat("context API", protectedClasses, hasItem("org.context.specific.")); assertThat("deprecated API", protectedClasses, hasItem("org.deprecated.api.")); } + + public static class OkServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + resp.setStatus(200); + } + } } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorHandler.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorHandler.java index adf4923fecb5..5c33b1615ac3 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorHandler.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorHandler.java @@ -14,14 +14,9 @@ package org.eclipse.jetty.ee11.servlet; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.io.UncheckedIOException; import java.io.Writer; -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.List; import jakarta.servlet.ServletException; @@ -29,9 +24,6 @@ import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpHeader; 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.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; @@ -75,35 +67,40 @@ public boolean handle(Request request, Response response, Callback callback) thr // Look for an error page dispatcher // This logic really should be in ErrorPageErrorHandler, but some implementations extend ErrorHandler // and implement ErrorPageMapper directly, so we do this here in the base class. - String errorPage = (this instanceof ErrorPageMapper) ? ((ErrorPageMapper)this).getErrorPage(httpServletRequest) : null; ServletContextHandler.ServletScopedContext context = servletContextRequest.getErrorContext(); - Dispatcher errorDispatcher = (errorPage != null && context != null) - ? (Dispatcher)context.getServletContext().getRequestDispatcher(errorPage) : null; - - if (errorDispatcher != null) + Integer errorStatus = (Integer)request.getAttribute(ERROR_STATUS); + Throwable errorCause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + if (this instanceof ErrorPageMapper mapper) { - try + ErrorPageMapper.ErrorPage errorPage = mapper.getErrorPage(errorStatus, errorCause); + if (LOG.isDebugEnabled()) + LOG.debug("{} {} {} -> {}", context, errorStatus, errorCause, errorPage); + if (errorPage != null && context.getServletContext().getRequestDispatcher(errorPage.errorPage) instanceof Dispatcher errorDispatcher) { try { - contextHandler.requestInitialized(servletContextRequest, httpServletRequest); - errorDispatcher.error(httpServletRequest, httpServletResponse); - } - finally - { - contextHandler.requestDestroyed(servletContextRequest, httpServletRequest); + try + { + mapper.prepare(errorPage, httpServletRequest, httpServletResponse); + contextHandler.requestInitialized(servletContextRequest, httpServletRequest); + errorDispatcher.error(httpServletRequest, httpServletResponse); + } + finally + { + contextHandler.requestDestroyed(servletContextRequest, httpServletRequest); + } + callback.succeeded(); + return true; } - callback.succeeded(); - return true; - } - catch (ServletException e) - { - if (LOG.isDebugEnabled()) - LOG.debug("Unable to call error dispatcher", e); - if (response.isCommitted()) + catch (ServletException e) { - callback.failed(e); - return true; + if (LOG.isDebugEnabled()) + LOG.debug("Unable to call error dispatcher", e); + if (response.isCommitted()) + { + callback.failed(e); + return true; + } } } } @@ -165,7 +162,19 @@ protected void writeErrorHtmlMessage(Request request, Writer writer, int code, S public interface ErrorPageMapper { - String getErrorPage(HttpServletRequest request); + enum PageLookupTechnique + { + THROWABLE, STATUS_CODE, GLOBAL + } + + record ErrorPage(String errorPage, PageLookupTechnique match, Throwable error, Throwable cause, Class matchedClass) + { + } + + ErrorPage getErrorPage(Integer errorStatusCode, Throwable error); + + default void prepare(ErrorPage errorPage, HttpServletRequest request, HttpServletResponse response) + {} } public static Request.Handler getErrorHandler(Server server, ContextHandler context) diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorPageErrorHandler.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorPageErrorHandler.java index 51f1a6f8f2c2..0c06e1007184 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorPageErrorHandler.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ErrorPageErrorHandler.java @@ -20,11 +20,10 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS; - /** * An ErrorHandler that maps exceptions and status codes to URIs for dispatch using * the internal ERROR style of dispatch. @@ -34,11 +33,6 @@ public class ErrorPageErrorHandler extends ErrorHandler implements ErrorHandler. public static final String GLOBAL_ERROR_PAGE = "org.eclipse.jetty.server.error_page.global"; private static final Logger LOG = LoggerFactory.getLogger(ErrorPageErrorHandler.class); - private enum PageLookupTechnique - { - THROWABLE, STATUS_CODE, GLOBAL - } - private final Map _errorPages = new HashMap<>(); // code or exception to URL private final List _errorPageList = new ArrayList<>(); // list of ErrorCode by range private boolean _unwrapServletException = true; @@ -60,118 +54,62 @@ public void setUnwrapServletException(boolean unwrapServletException) } @Override - public String getErrorPage(HttpServletRequest request) + public void prepare(ErrorPage errorPage, HttpServletRequest request, HttpServletResponse response) { - String errorPage = null; - - PageLookupTechnique pageSource = null; - - Class matchedThrowable = null; - Throwable error = (Throwable)request.getAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION); - Throwable cause = error; - - // Walk the cause hierarchy - while (errorPage == null && cause != null) - { - pageSource = PageLookupTechnique.THROWABLE; - - Class exClass = cause.getClass(); - errorPage = _errorPages.get(exClass.getName()); - - // walk the inheritance hierarchy - while (errorPage == null) - { - exClass = exClass.getSuperclass(); - if (exClass == null) - break; - errorPage = _errorPages.get(exClass.getName()); - } - - if (errorPage != null) - matchedThrowable = exClass; - - cause = (cause instanceof ServletException) ? ((ServletException)cause).getRootCause() : null; - } - - if (error instanceof ServletException && _unwrapServletException) + if (errorPage.error() instanceof ServletException && _unwrapServletException) { - Throwable unwrapped = unwrapServletException(error, matchedThrowable); + Throwable unwrapped = unwrapServletException(errorPage.error(), errorPage.matchedClass()); if (unwrapped != null) { request.setAttribute(org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION, unwrapped); request.setAttribute(Dispatcher.ERROR_EXCEPTION_TYPE, unwrapped.getClass()); } } + } - Integer errorStatusCode = null; + @Override + public ErrorPage getErrorPage(Integer errorStatusCode, Throwable error) + { + String errorPage; - if (errorPage == null) + // Walk the cause hierarchy + Throwable cause = error; + while (cause != null) { - pageSource = PageLookupTechnique.STATUS_CODE; + Class exClass = cause.getClass(); - // look for an exact code match - errorStatusCode = (Integer)request.getAttribute(ERROR_STATUS); - if (errorStatusCode != null) + while (exClass != null) { - errorPage = _errorPages.get(Integer.toString(errorStatusCode)); - - // if still not found - if (errorPage == null) - { - // look for an error code range match. - for (ErrorCodeRange errCode : _errorPageList) - { - if (errCode.isInRange(errorStatusCode)) - { - errorPage = errCode.getUri(); - break; - } - } - } + errorPage = _errorPages.get(exClass.getName()); + if (errorPage != null) + return new ErrorPage(errorPage, PageLookupTechnique.THROWABLE, error, cause, exClass); + exClass = exClass.getSuperclass(); } - } - // Try servlet 3.x global error page. - if (errorPage == null) - { - pageSource = PageLookupTechnique.GLOBAL; - errorPage = _errorPages.get(GLOBAL_ERROR_PAGE); + cause = (cause instanceof ServletException se) ? se.getRootCause() : cause.getCause(); } - if (LOG.isDebugEnabled()) + // look for an exact code match + if (errorStatusCode != null) { - StringBuilder dbg = new StringBuilder(); - dbg.append("getErrorPage("); - dbg.append(request.getMethod()).append(' '); - dbg.append(request.getRequestURI()); - dbg.append(") => error_page=").append(errorPage); - switch (pageSource) + errorPage = _errorPages.get(Integer.toString(errorStatusCode)); + if (errorPage != null) + return new ErrorPage(errorPage, PageLookupTechnique.STATUS_CODE, error, error, null); + + // look for an error code range match. + for (ErrorCodeRange errCode : _errorPageList) { - case THROWABLE: - dbg.append(" (using matched Throwable "); - dbg.append(matchedThrowable.getName()); - dbg.append(" / actually thrown as "); - Throwable originalThrowable = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - dbg.append(originalThrowable.getClass().getName()); - dbg.append(')'); - LOG.debug(dbg.toString(), cause); - break; - case STATUS_CODE: - dbg.append(" (from status code "); - dbg.append(errorStatusCode); - dbg.append(')'); - LOG.debug(dbg.toString()); - break; - case GLOBAL: - dbg.append(" (from global default)"); - LOG.debug(dbg.toString()); - break; - default: - throw new IllegalStateException(pageSource.toString()); + if (errCode.isInRange(errorStatusCode)) + return new ErrorPage(errCode.getUri(), PageLookupTechnique.STATUS_CODE, error, error, null); } } - return errorPage; + // Try servlet 3.x global error page. + errorPage = _errorPages.get(GLOBAL_ERROR_PAGE); + if (errorPage != null) + return new ErrorPage(errorPage, PageLookupTechnique.GLOBAL, error, error, null); + + return null; } /** diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java index e7472e9d3eb6..6e3b0e679878 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java @@ -350,8 +350,8 @@ public PrintWriter getWriter() throws IOException // We must use an implementation of AbstractOutputStreamWriter here as we rely on the non cached characters // in the writer implementation for flush and completion operations. WriteThroughWriter outputStreamWriter = WriteThroughWriter.newWriter(getServletChannel().getHttpOutput(), encoding); - getServletResponseInfo().setWriter(writer = new ResponseWriter( - outputStreamWriter, locale, encoding)); + writer = new ResponseWriter(outputStreamWriter, locale, encoding); + getServletResponseInfo().setWriter(writer); } // Set the output type at the end, because setCharacterEncoding() checks for it. diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletChannelState.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletChannelState.java index fb55b9b9be48..d9d9bfc74116 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletChannelState.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletChannelState.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_EXCEPTION; -import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_ORIGIN; +import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS; /** * holder of the state of request-response cycle. @@ -419,6 +419,16 @@ public Action handling() throw new IllegalStateException(getStatusStringLocked()); _initial = true; _state = State.HANDLING; + if (_servletChannel.getResponse().getStatus() != 0) + { + if (_servletChannel.getRequest().getAttribute(ERROR_STATUS) instanceof Integer errorCode) + { + _servletChannel.getServletRequestState().sendError(errorCode, null); + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + } + } return Action.DISPATCH; case WOKEN: @@ -1015,7 +1025,6 @@ else if (cause instanceof UnavailableException) 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: @@ -1051,7 +1060,7 @@ public void sendError(int code, String message) response.setStatus(code); servletContextRequest.errorClose(); - request.setAttribute(ERROR_ORIGIN, servletContextRequest.getServletName()); + request.setAttribute(ErrorHandler.ERROR_ORIGIN, servletContextRequest.getServletName()); request.setAttribute(ErrorHandler.ERROR_CONTEXT, servletContextRequest.getServletContext()); request.setAttribute(ErrorHandler.ERROR_MESSAGE, message); request.setAttribute(ErrorHandler.ERROR_STATUS, code); @@ -1059,7 +1068,7 @@ public void sendError(int code, String message) _sendError = true; if (_event != null) { - Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + Throwable cause = (Throwable)request.getAttribute(ErrorHandler.ERROR_EXCEPTION); if (cause != null) _event.addThrowable(cause); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextHandler.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextHandler.java index 984a058736c9..7c4033d38056 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextHandler.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextHandler.java @@ -73,6 +73,7 @@ import org.eclipse.jetty.ee11.servlet.security.ConstraintAware; import org.eclipse.jetty.ee11.servlet.security.ConstraintMapping; import org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.io.IOResources; @@ -113,6 +114,7 @@ import org.slf4j.LoggerFactory; import static jakarta.servlet.ServletContext.TEMPDIR; +import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_STATUS; /** * Servlet Context. @@ -1145,7 +1147,6 @@ protected ContextRequest wrapRequest(Request request, Response response) decodedPathInContext = URIUtil.decodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath())); matchedResource = _servletHandler.getMatchedServlet(decodedPathInContext); - if (matchedResource == null) return wrapNoServlet(request, response); ServletHandler.MappedServlet mappedServlet = matchedResource.getResource(); @@ -1195,7 +1196,23 @@ protected boolean handleByContextHandler(String pathInContext, ContextRequest re if (!initialDispatch) return false; - return super.handleByContextHandler(pathInContext, request, response, callback); + if (isProtectedTarget(pathInContext)) + { + if (getErrorHandler() instanceof ErrorHandler.ErrorPageMapper mapper) + { + ErrorHandler.ErrorPageMapper.ErrorPage errorPage = mapper.getErrorPage(404, null); + if (errorPage != null) + { + // Do nothing here other than set the error status so that the ServletHandler will handle as if a sendError + request.setAttribute(ERROR_STATUS, 404); + response.setStatus(HttpStatus.NOT_FOUND_404); + return false; + } + } + Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404, null); + return true; + } + return false; } @Override diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java index 36f80112d300..3a12b608105a 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ErrorPageTest.java @@ -74,6 +74,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -2006,6 +2007,68 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) assertThat(response.getStatus(), is(598)); } + @Test + public void testProtectedTargetNoPage() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/ctx"); + contextHandler.setProtectedTargets(new String[] {"/WEB-INF", "/META-INF"}); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(new OkServlet(), "/*"); + + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + contextHandler.setErrorHandler(errorPageErrorHandler); + + startServer(contextHandler); + + String rawRequest = """ + GET /ctx/WEB-INF/anything HTTP/1.1\r + Host: test\r + Connection: close\r + Accept: */*\r + Accept-Charset: *\r + \r + """; + + String rawResponse = _connector.getResponse(rawRequest); + assertThat(rawResponse, startsWith("HTTP/1.1 404 Not Found")); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getContent(), containsString("

HTTP ERROR 404 Not Found

")); + } + + @Test + public void testProtectedTarget() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/ctx"); + contextHandler.setProtectedTargets(new String[] {"/WEB-INF", "/META-INF"}); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(new OkServlet(), "/*"); + + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + contextHandler.setErrorHandler(errorPageErrorHandler); + errorPageErrorHandler.addErrorPage(404, "/error/404"); + + startServer(contextHandler); + + String rawRequest = """ + GET /ctx/WEB-INF/anything HTTP/1.1\r + Host: test\r + Connection: close\r + Accept: */*\r + Accept-Charset: *\r + \r + """; + + String rawResponse = _connector.getResponse(rawRequest); + assertThat(rawResponse, startsWith("HTTP/1.1 404 Not Found")); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getContent(), containsString("ERROR_PAGE: /404")); + assertThat(response.getContent(), containsString("ERROR_MESSAGE: Not Found")); + } + public static class ErrorDumpServlet extends HttpServlet { @Override @@ -2125,4 +2188,13 @@ public TestServletException(Throwable rootCause) super(rootCause); } } + + public static class OkServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + resp.setStatus(200); + } + } } diff --git a/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/WebAppContextTest.java b/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/WebAppContextTest.java index 20252099aff5..0dff23b056c8 100644 --- a/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/WebAppContextTest.java +++ b/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/WebAppContextTest.java @@ -15,6 +15,7 @@ import java.io.File; import java.io.IOException; +import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.nio.file.FileSystem; @@ -32,16 +33,26 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.DispatcherType; import jakarta.servlet.GenericServlet; import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee.WebAppClassLoading; +import org.eclipse.jetty.ee11.servlet.DefaultServlet; +import org.eclipse.jetty.ee11.servlet.Dispatcher; import org.eclipse.jetty.ee11.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee11.servlet.ServletChannel; import org.eclipse.jetty.ee11.servlet.ServletContextHandler; +import org.eclipse.jetty.ee11.servlet.ServletHolder; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; @@ -84,6 +95,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -153,6 +165,51 @@ private Path createWar(Path tempDir, String name) throws Exception return warFile; } + @Test + public void testProtectedTargetErrorPage() throws Exception + { + WebAppContext contextHandler = new WebAppContext(); + contextHandler.setContextPath("/foo"); + contextHandler.setBaseResourceAsPath(Path.of("/tmp")); + ServletHolder defaultHolder = new ServletHolder(new DefaultServlet()); + defaultHolder.setDisplayName("default"); + + contextHandler.addServlet(defaultHolder, "/"); + contextHandler.addServlet(new OkServlet(), "/*"); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(GlobalErrorDumpServlet.class, "/global/*"); + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + errorPageErrorHandler.addErrorPage(404, "/error/TestException"); + errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/global/TestException"); + contextHandler.setErrorHandler(errorPageErrorHandler); + Server server = new Server(); + server.setHandler(contextHandler); + + LocalConnector connector = new LocalConnector(server); + server.addConnector(connector); + server.start(); + + try (StacklessLogging stackless = new StacklessLogging(ServletChannel.class)) + { + StringBuilder rawRequest = new StringBuilder(); + rawRequest.append("GET /foo/WEB-INF/classes/this/does/not/exist").append(" HTTP/1.1\r\n"); + rawRequest.append("Host: test\r\n"); + rawRequest.append("Connection: close\r\n"); + rawRequest.append("\r\n"); + + String rawResponse = connector.getResponse(rawRequest.toString()); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getValuesList("ERRORDUMPSERVLET"), contains("ERRORDUMPSERVLET")); + String content = response.getContent(); + assertThat(content, containsString("ERROR_REQUEST_URI: /foo/WEB-INF/classes/this/does/not/exist")); + assertThat(content, containsString("getRequestURI()=[/foo/error/TestException]")); + assertThat(content, containsString("DISPATCH: ERROR")); + assertThat(content, not(containsString("GLOBALERRORDUMPSERVLET"))); + } + } + @Test public void testDefaultContextPath() throws Exception { @@ -518,6 +575,95 @@ public void service(ServletRequest req, ServletResponse res) } } + public static class ErrorDumpServlet extends HttpServlet + { + @Override + 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()); + + response.setHeader("ERRORDUMPSERVLET", "ERRORDUMPSERVLET"); + + PrintWriter writer = response.getWriter(); + writer.println("DISPATCH: " + request.getDispatcherType().name()); + writer.println("ERROR_PAGE: " + request.getPathInfo()); + writer.println(request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println(request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + + protected String valueOf(Object obj) + { + if (obj == null) + return "null"; + return valueOf(obj.toString()); + } + + protected String valueOf(String str) + { + if (str == null) + return "null"; + return String.format("[%s]", str); + } + } + + public static class GlobalErrorDumpServlet extends ErrorDumpServlet + { + @Override + 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()); + + response.setHeader("GLOBALERRORDUMPSERVLET", "GLOBALERRORDUMPSERVLET"); + PrintWriter writer = response.getWriter(); + writer.println("GLOBAL DISPATCH: " + request.getDispatcherType().name()); + writer.println("GLOBAL ERROR_PAGE: " + request.getPathInfo()); + writer.println("GLOBAL ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("GLOBAL ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("GLOBAL ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("GLOBAL ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("GLOBAL ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("GLOBAL ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + } + @Test public void testBaseResourceAbsolutePath(WorkDir workDir) throws Exception { @@ -1006,4 +1152,13 @@ public void testAddProtectedClasses() throws Exception assertThat("context API", protectedClasses, hasItem("org.context.specific.")); } + + public static class OkServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + { + resp.setStatus(200); + } + } } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java index f227f19bdb6b..8092512b3b41 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java @@ -2681,6 +2681,13 @@ public class CoreContextHandler extends org.eclipse.jetty.server.handler.Context installBean(ContextHandler.this, true); } + @Override + protected boolean handleByContextHandler(String pathInContext, ContextRequest request, Response response, Callback callback) + { + // The CoreContextHandler should never handle the request. Defer to the nested ContextHandler to do so if necessary. + return false; + } + @Override public void makeTempDirectory() throws Exception { diff --git a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java index 04e0ef43ffed..42869092f7c2 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java +++ b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java @@ -14,6 +14,8 @@ package org.eclipse.jetty.ee9.webapp; import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.nio.file.FileSystem; @@ -30,17 +32,26 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.DispatcherType; import jakarta.servlet.GenericServlet; import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee.WebAppClassLoading; import org.eclipse.jetty.ee9.nested.ContextHandler; +import org.eclipse.jetty.ee9.nested.Dispatcher; +import org.eclipse.jetty.ee9.servlet.DefaultServlet; import org.eclipse.jetty.ee9.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; @@ -82,6 +93,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -547,6 +559,50 @@ public void testNullSessionAndSecurityHandler() throws Exception assertTrue(context.isAvailable()); } + @Test + public void testErrorPage() throws Exception + { + WebAppContext contextHandler = new WebAppContext(); + contextHandler.setContextPath("/foo"); + contextHandler.setBaseResourceAsPath(Path.of("/tmp")); + ServletHolder defaultHolder = new ServletHolder(new DefaultServlet()); + defaultHolder.setDisplayName("default"); + + contextHandler.addServlet(defaultHolder, "/"); + contextHandler.addServlet(ErrorDumpServlet.class, "/error/*"); + contextHandler.addServlet(GlobalErrorDumpServlet.class, "/global/*"); + ErrorPageErrorHandler errorPageErrorHandler = new ErrorPageErrorHandler(); + errorPageErrorHandler.addErrorPage(404, "/error/TestException"); + errorPageErrorHandler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, "/global/TestException"); + contextHandler.setErrorHandler(errorPageErrorHandler); + Server server = new Server(); + server.setHandler(contextHandler); + + LocalConnector connector = new LocalConnector(server); + server.addConnector(connector); + server.start(); + + try (StacklessLogging stackless = new StacklessLogging(WebAppContext.class)) + { + StringBuilder rawRequest = new StringBuilder(); + rawRequest.append("GET /foo/WEB-INF/classes/this/does/not/exist").append(" HTTP/1.1\r\n"); + rawRequest.append("Host: test\r\n"); + rawRequest.append("Connection: close\r\n"); + rawRequest.append("\r\n"); + + String rawResponse = connector.getResponse(rawRequest.toString()); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getStatus(), is(404)); + assertThat(response.getValuesList("ERRORDUMPSERVLET"), contains("ERRORDUMPSERVLET")); + String content = response.getContent(); + assertThat(content, containsString("ERROR_REQUEST_URI: /foo/WEB-INF/classes/this/does/not/exist")); + assertThat(content, containsString("getRequestURI()=[/foo/error/TestException]")); + assertThat(content, containsString("DISPATCH: ERROR")); + assertThat(content, not(containsString("GLOBALERRORDUMPSERVLET"))); + } + } + static class ServletA extends GenericServlet { @Override @@ -565,6 +621,95 @@ public void service(ServletRequest req, ServletResponse res) } } + public static class ErrorDumpServlet extends HttpServlet + { + @Override + 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()); + + response.setHeader("ERRORDUMPSERVLET", "ERRORDUMPSERVLET"); + + PrintWriter writer = response.getWriter(); + writer.println("DISPATCH: " + request.getDispatcherType().name()); + writer.println("ERROR_PAGE: " + request.getPathInfo()); + writer.println(request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println(request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + + protected String valueOf(Object obj) + { + if (obj == null) + return "null"; + return valueOf(obj.toString()); + } + + protected String valueOf(String str) + { + if (str == null) + return "null"; + return String.format("[%s]", str); + } + } + + public static class GlobalErrorDumpServlet extends ErrorDumpServlet + { + @Override + 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()); + + response.setHeader("GLOBALERRORDUMPSERVLET", "GLOBALERRORDUMPSERVLET"); + PrintWriter writer = response.getWriter(); + writer.println("GLOBAL DISPATCH: " + request.getDispatcherType().name()); + writer.println("GLOBAL ERROR_PAGE: " + request.getPathInfo()); + writer.println("GLOBAL ERROR_MESSAGE: " + request.getAttribute(Dispatcher.ERROR_MESSAGE)); + writer.println("GLOBAL ERROR_CODE: " + request.getAttribute(Dispatcher.ERROR_STATUS_CODE)); + writer.println("GLOBAL ERROR_EXCEPTION: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION)); + writer.println("GLOBAL ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); + writer.println("GLOBAL ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + writer.println("GLOBAL ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + + writer.printf("getRequestURI()=%s%n", valueOf(request.getRequestURI())); + writer.printf("getRequestURL()=%s%n", valueOf(request.getRequestURL())); + writer.printf("getQueryString()=%s%n", valueOf(request.getQueryString())); + Map params = request.getParameterMap(); + writer.printf("getParameterMap().size=%d%n", params.size()); + for (Map.Entry entry : params.entrySet()) + { + String value = null; + if (entry.getValue() != null) + { + value = String.join(", ", entry.getValue()); + } + writer.printf("getParameterMap()[%s]=%s%n", entry.getKey(), valueOf(value)); + } + } + } + @Test public void testBaseResourceAbsolutePath(WorkDir workDir) throws Exception {