From 74232b7adc25570b3c71b252ecbe39a2bce72404 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 26 Aug 2025 22:49:03 +1000 Subject: [PATCH 1/3] Issue #13509 - Fix multipart usage with FORWARD and INCLUDE dispatch. Signed-off-by: Lachlan Roberts --- .../jetty/ee10/servlet/Dispatcher.java | 40 ++++- .../jetty/ee10/servlet/ServletApiRequest.java | 5 +- .../ee10/servlet/MultiPartServletTest.java | 161 +++++++++++++++++- 3 files changed, 197 insertions(+), 9 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 b03cf6707f4d..7cad5713aeae 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 @@ -45,7 +45,6 @@ import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.UrlEncoded; public class Dispatcher implements RequestDispatcher @@ -59,7 +58,7 @@ public class Dispatcher implements RequestDispatcher * Dispatch include attribute names */ public static final String __FORWARD_PREFIX = "jakarta.servlet.forward."; - + /** * Name of original request attribute */ @@ -67,6 +66,11 @@ public class Dispatcher implements RequestDispatcher public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; + /** + * This attribute is used to store the wrapped request for internal use during a dispatch. + */ + public static final String WRAPPED_REQUEST_ATTRIBUTE = "org.eclipse.jetty.server.wrappedRequest"; + private final ServletContextHandler _contextHandler; private final HttpURI _uri; private final String _decodedPathInContext; @@ -124,7 +128,18 @@ public void forward(ServletRequest request, ServletResponse response) throws Ser ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request); servletContextRequest.getServletContextResponse().resetForForward(); - _mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse); + + Object oldWrappedRequest = httpRequest.getAttribute(WRAPPED_REQUEST_ATTRIBUTE); + try + { + ForwardRequest forwardRequest = new ForwardRequest(httpRequest); + httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, forwardRequest); + _mappedServlet.handle(_servletHandler, _decodedPathInContext, forwardRequest, httpResponse); + } + finally + { + httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, oldWrappedRequest); + } // If we are not async and not closed already, then close via the possibly wrapped response. if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getServletContextResponse().hasLastWrite()) @@ -150,12 +165,16 @@ public void include(ServletRequest request, ServletResponse response) throws Ser ServletContextResponse servletContextResponse = ServletContextResponse.getServletContextResponse(response); IncludeResponse includeResponse = new IncludeResponse(httpResponse); + Object oldWrappedRequest = httpRequest.getAttribute(WRAPPED_REQUEST_ATTRIBUTE); try { - _mappedServlet.handle(_servletHandler, _decodedPathInContext, new IncludeRequest(httpRequest), includeResponse); + IncludeRequest includeRequest = new IncludeRequest(httpRequest); + httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, includeRequest); + _mappedServlet.handle(_servletHandler, _decodedPathInContext, includeRequest, includeResponse); } finally { + httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, oldWrappedRequest); includeResponse.onIncluded(); servletContextResponse.included(); } @@ -434,6 +453,14 @@ public Object getAttribute(String name) case RequestDispatcher.INCLUDE_REQUEST_URI -> (_uri == null) ? null : _uri.getPath(); case RequestDispatcher.INCLUDE_CONTEXT_PATH -> _httpServletRequest.getContextPath(); case RequestDispatcher.INCLUDE_QUERY_STRING -> (_uri == null) ? null : _uri.getQuery(); + case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> + { + // If we already have future parts, return the configuration of the wrapped request. + if (super.getAttribute(ServletMultiPartFormData.class.getName()) != null) + yield super.getAttribute(name); + // otherwise, return the configuration of this mapping + yield _mappedServlet.getServletHolder().getMultipartConfigElement(); + } default -> super.getAttribute(name); }; } @@ -443,6 +470,11 @@ public Enumeration getAttributeNames() { //Servlet Spec 9.3.1 no include attributes if a named dispatcher ArrayList names = new ArrayList<>(Collections.list(super.getAttributeNames())); + + //only return the multipart attribute name if this servlet mapping has multipart config + if (names.contains(ServletContextRequest.MULTIPART_CONFIG_ELEMENT) && _mappedServlet.getServletHolder().getMultipartConfigElement() == null) + names.remove(ServletContextRequest.MULTIPART_CONFIG_ELEMENT); + if (_named != null) return Collections.enumeration(names); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 942421bfffb8..abaddfa9ccc0 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -94,6 +94,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.ee10.servlet.Dispatcher.WRAPPED_REQUEST_ATTRIBUTE; + /** * The Jetty implementation of the ee10 {@link HttpServletRequest} object. * This provides the bridge from Servlet {@link HttpServletRequest} to the Jetty Core {@link Request} @@ -634,7 +636,8 @@ public Collection getParts() throws IOException, ServletException { try { - _parts = ServletMultiPartFormData.getParts(this); + ServletRequest dispatchedRequest = (ServletRequest)getAttribute(WRAPPED_REQUEST_ATTRIBUTE); + _parts = ServletMultiPartFormData.getParts(dispatchedRequest == null ? this : dispatchedRequest); Collection parts = _parts.getParts(); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java index 662ce20e58fb..084e522b9039 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java @@ -56,6 +56,7 @@ import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.IO; @@ -96,15 +97,23 @@ public void before() throws Exception private void start(HttpServlet servlet, MultipartConfigElement config, boolean eager) throws Exception { - config = config == null ? new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config; + start(servletContextHandler -> + { + ServletHolder servletHolder = new ServletHolder(servlet); + servletHolder.getRegistration().setMultipartConfig(config == null ? + new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config); + servletContextHandler.addServlet(servletHolder, "/"); + }, eager); + } + + private void start(Consumer consumer, boolean eager) throws Exception + { server = new Server(null, null, null); connector = new ServerConnector(server); server.addConnector(connector); ServletContextHandler servletContextHandler = new ServletContextHandler("/"); - ServletHolder servletHolder = new ServletHolder(servlet); - servletHolder.getRegistration().setMultipartConfig(config); - servletContextHandler.addServlet(servletHolder, "/"); + consumer.accept(servletContextHandler); server.setHandler(servletContextHandler); GzipHandler gzipHandler = new GzipHandler(); @@ -570,4 +579,148 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I assertThat(responseContent, containsString("Parameter: part3=" + contentString)); assertThat(responseContent, not(containsString("Parameter: partFileName=" + contentString))); } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testForwardDispatch(boolean eager) throws Exception + { + start(servletContextHandler -> + { + servletContextHandler.addServlet(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + request.getRequestDispatcher("/multipart").forward(request, response); + } + }, "/"); + + ServletHolder servletHolder = new ServletHolder(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + assertNotNull(parts); + assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + assertEquals("part1", part.getName()); + Collection headerNames = part.getHeaderNames(); + assertNotNull(headerNames); + assertEquals(2, headerNames.size()); + String content1 = IO.toString(part.getInputStream(), UTF_8); + assertEquals("content1", content1); + response.getWriter().print("success!"); + } + }); + servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0)); + servletContextHandler.addServlet(servletHolder, "/multipart"); + }, eager); + + if (server.getErrorHandler() instanceof ErrorHandler errorHandler) + errorHandler.setShowStacks(true); + + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String content = """ + --A1B2C3 + Content-Disposition: form-data; name="part1" + Content-Type: text/plain; charset="UTF-8" + + content1 + --A1B2C3-- + """; + String header = """ + POST / HTTP/1.1 + Host: localhost + Content-Type: multipart/form-data; boundary="A1B2C3" + Content-Length: $L + + """.replace("$L", String.valueOf(content.length())); + + output.write(header.getBytes(UTF_8)); + output.write(content.getBytes(UTF_8)); + output.flush(); + + HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream()); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContent(), equalTo("success!")); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testIncludeDispatch(boolean eager) throws Exception + { + start(servletContextHandler -> + { + servletContextHandler.addServlet(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + request.getRequestDispatcher("/multipart").include(request, response); + } + }, "/"); + + ServletHolder servletHolder = new ServletHolder(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + assertNotNull(parts); + assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + assertEquals("part1", part.getName()); + Collection headerNames = part.getHeaderNames(); + assertNotNull(headerNames); + assertEquals(2, headerNames.size()); + String content1 = IO.toString(part.getInputStream(), UTF_8); + assertEquals("content1", content1); + response.getWriter().print("success!"); + } + }); + servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0)); + servletContextHandler.addServlet(servletHolder, "/multipart"); + }, eager); + + if (server.getErrorHandler() instanceof ErrorHandler errorHandler) + errorHandler.setShowStacks(true); + + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String content = """ + --A1B2C3 + Content-Disposition: form-data; name="part1" + Content-Type: text/plain; charset="UTF-8" + + content1 + --A1B2C3-- + """; + String header = """ + POST / HTTP/1.1 + Host: localhost + Content-Type: multipart/form-data; boundary="A1B2C3" + Content-Length: $L + + """.replace("$L", String.valueOf(content.length())); + + output.write(header.getBytes(UTF_8)); + output.write(content.getBytes(UTF_8)); + output.flush(); + + HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream()); + assertNotNull(response); + System.err.println(response); + System.err.println(response.getContent()); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContent(), equalTo("success!")); + } + } } From f019c724d74a63a7e9ace8667602007b73790277 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 27 Aug 2025 12:31:27 +1000 Subject: [PATCH 2/3] PR #13518 - fix checkstyle error Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java index 084e522b9039..af7b3ecb761d 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java @@ -100,8 +100,8 @@ private void start(HttpServlet servlet, MultipartConfigElement config, boolean e start(servletContextHandler -> { ServletHolder servletHolder = new ServletHolder(servlet); - servletHolder.getRegistration().setMultipartConfig(config == null ? - new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config); + servletHolder.getRegistration().setMultipartConfig(config == null + ? new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config); servletContextHandler.addServlet(servletHolder, "/"); }, eager); } From e52b660994e60d046342d0c1e885f7639152cbf2 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 27 Aug 2025 17:03:32 +1000 Subject: [PATCH 3/3] PR #13518 - changes from review, and implement for ASYNC dispatch type Signed-off-by: Lachlan Roberts --- .../jetty/ee10/servlet/Dispatcher.java | 120 +++++++++++++++--- .../ee10/servlet/MultiPartServletTest.java | 98 ++++---------- 2 files changed, 127 insertions(+), 91 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 7cad5713aeae..72b12e081708 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 @@ -19,6 +19,7 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -38,6 +39,7 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.http.Part; import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; @@ -67,7 +69,7 @@ public class Dispatcher implements RequestDispatcher public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; /** - * This attribute is used to store the wrapped request for internal use during a dispatch. + * This attribute is used to store the wrapped request for internal use during a dispatch if needed. */ public static final String WRAPPED_REQUEST_ATTRIBUTE = "org.eclipse.jetty.server.wrappedRequest"; @@ -128,18 +130,7 @@ public void forward(ServletRequest request, ServletResponse response) throws Ser ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request); servletContextRequest.getServletContextResponse().resetForForward(); - - Object oldWrappedRequest = httpRequest.getAttribute(WRAPPED_REQUEST_ATTRIBUTE); - try - { - ForwardRequest forwardRequest = new ForwardRequest(httpRequest); - httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, forwardRequest); - _mappedServlet.handle(_servletHandler, _decodedPathInContext, forwardRequest, httpResponse); - } - finally - { - httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, oldWrappedRequest); - } + _mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse); // If we are not async and not closed already, then close via the possibly wrapped response. if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getServletContextResponse().hasLastWrite()) @@ -165,16 +156,12 @@ public void include(ServletRequest request, ServletResponse response) throws Ser ServletContextResponse servletContextResponse = ServletContextResponse.getServletContextResponse(response); IncludeResponse includeResponse = new IncludeResponse(httpResponse); - Object oldWrappedRequest = httpRequest.getAttribute(WRAPPED_REQUEST_ATTRIBUTE); try { - IncludeRequest includeRequest = new IncludeRequest(httpRequest); - httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, includeRequest); - _mappedServlet.handle(_servletHandler, _decodedPathInContext, includeRequest, includeResponse); + _mappedServlet.handle(_servletHandler, _decodedPathInContext, new IncludeRequest(httpRequest), includeResponse); } finally { - httpRequest.setAttribute(WRAPPED_REQUEST_ATTRIBUTE, oldWrappedRequest); includeResponse.onIncluded(); servletContextResponse.included(); } @@ -417,6 +404,34 @@ public Enumeration getAttributeNames() names.add(RequestDispatcher.FORWARD_QUERY_STRING); return Collections.enumeration(names); } + + @Override + public Collection getParts() throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getParts(); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getPart(name); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } } private class IncludeRequest extends ParameterRequestWrapper @@ -492,6 +507,34 @@ public String getQueryString() { return _httpServletRequest.getQueryString(); } + + @Override + public Collection getParts() throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getParts(); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getPart(name); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } } private static class IncludeResponse extends HttpServletResponseWrapper @@ -770,6 +813,14 @@ public Object getAttribute(String name) case AsyncContextState.ASYNC_PATH_INFO -> _httpServletRequest.getPathInfo(); case AsyncContextState.ASYNC_SERVLET_PATH -> _httpServletRequest.getServletPath(); case AsyncContextState.ASYNC_QUERY_STRING -> _httpServletRequest.getQueryString(); + case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> + { + // If we already have future parts, return the configuration of the wrapped request. + if (super.getAttribute(ServletMultiPartFormData.class.getName()) != null) + yield super.getAttribute(name); + // otherwise, return the configuration of this mapping + yield _mappedServlet.getServletHolder().getMultipartConfigElement(); + } default -> super.getAttribute(name); }; } @@ -778,6 +829,11 @@ public Object getAttribute(String name) public Enumeration getAttributeNames() { ArrayList names = new ArrayList<>(Collections.list(super.getAttributeNames())); + + //only return the multipart attribute name if this servlet mapping has multipart config + if (names.contains(ServletContextRequest.MULTIPART_CONFIG_ELEMENT) && _mappedServlet.getServletHolder().getMultipartConfigElement() == null) + names.remove(ServletContextRequest.MULTIPART_CONFIG_ELEMENT); + names.add(AsyncContextState.ASYNC_REQUEST_URI); names.add(AsyncContextState.ASYNC_SERVLET_PATH); names.add(AsyncContextState.ASYNC_PATH_INFO); @@ -817,6 +873,34 @@ else if (getRequest() instanceof ServletApiRequest servletApiRequest) } return super.getParameters(); } + + @Override + public Collection getParts() throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getParts(); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + try + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, this); + return super.getPart(name); + } + finally + { + setAttribute(WRAPPED_REQUEST_ATTRIBUTE, null); + } + } } private class ErrorRequest extends ParameterRequestWrapper diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java index af7b3ecb761d..7b3a1b58bf66 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import jakarta.servlet.MultipartConfigElement; @@ -64,6 +65,8 @@ import org.junit.jupiter.api.AfterEach; 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 org.junit.jupiter.params.provider.ValueSource; import static java.nio.charset.StandardCharsets.UTF_8; @@ -580,80 +583,21 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I assertThat(responseContent, not(containsString("Parameter: partFileName=" + contentString))); } - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testForwardDispatch(boolean eager) throws Exception + public static Stream dispatchTestArgs() { - start(servletContextHandler -> - { - servletContextHandler.addServlet(new HttpServlet() - { - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException - { - request.getRequestDispatcher("/multipart").forward(request, response); - } - }, "/"); - - ServletHolder servletHolder = new ServletHolder(new HttpServlet() - { - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException - { - Collection parts = request.getParts(); - assertNotNull(parts); - assertEquals(1, parts.size()); - Part part = parts.iterator().next(); - assertEquals("part1", part.getName()); - Collection headerNames = part.getHeaderNames(); - assertNotNull(headerNames); - assertEquals(2, headerNames.size()); - String content1 = IO.toString(part.getInputStream(), UTF_8); - assertEquals("content1", content1); - response.getWriter().print("success!"); - } - }); - servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0)); - servletContextHandler.addServlet(servletHolder, "/multipart"); - }, eager); - - if (server.getErrorHandler() instanceof ErrorHandler errorHandler) - errorHandler.setShowStacks(true); - - try (Socket socket = new Socket("localhost", connector.getLocalPort())) - { - OutputStream output = socket.getOutputStream(); - - String content = """ - --A1B2C3 - Content-Disposition: form-data; name="part1" - Content-Type: text/plain; charset="UTF-8" - - content1 - --A1B2C3-- - """; - String header = """ - POST / HTTP/1.1 - Host: localhost - Content-Type: multipart/form-data; boundary="A1B2C3" - Content-Length: $L - - """.replace("$L", String.valueOf(content.length())); - - output.write(header.getBytes(UTF_8)); - output.write(content.getBytes(UTF_8)); - output.flush(); - - HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream()); - assertNotNull(response); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getContent(), equalTo("success!")); - } + return Stream.of( + Arguments.of(true, "forward"), + Arguments.of(false, "forward"), + Arguments.of(true, "include"), + Arguments.of(false, "include"), + Arguments.of(true, "async"), + Arguments.of(false, "async") + ); } @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testIncludeDispatch(boolean eager) throws Exception + @MethodSource("dispatchTestArgs") + public void testDispatch(boolean eager, String dispatchType) throws Exception { start(servletContextHandler -> { @@ -662,7 +606,17 @@ public void testIncludeDispatch(boolean eager) throws Exception @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.getRequestDispatcher("/multipart").include(request, response); + switch (dispatchType) + { + case "forward" -> request.getRequestDispatcher("/multipart").forward(request, response); + case "include" -> request.getRequestDispatcher("/multipart").include(request, response); + case "async" -> + { + request.startAsync(); + request.getAsyncContext().dispatch("/multipart"); + } + default -> throw new ServletException("Unknown dispatch type: " + dispatchType); + } } }, "/"); @@ -717,8 +671,6 @@ protected void service(HttpServletRequest request, HttpServletResponse response) HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream()); assertNotNull(response); - System.err.println(response); - System.err.println(response.getContent()); assertEquals(HttpStatus.OK_200, response.getStatus()); assertThat(response.getContent(), equalTo("success!")); }