diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncExceptionWriterServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncExceptionWriterServlet.java new file mode 100644 index 0000000000..2573d240c6 --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncExceptionWriterServlet.java @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.servlet.test.response.writer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Asynchronous version of {@link ExceptionWriterServlet}. + * + * @author rmartinc + */ +public class AsyncExceptionWriterServlet extends jakarta.servlet.http.HttpServlet { + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + final var asyncContext = req.startAsync(); + new Thread(()->{ + try { + resp.setContentType("text/plain;charset=UTF-8"); + try (PrintWriter writer = resp.getWriter()) { + new Exception("TestException").printStackTrace(writer); + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + asyncContext.complete(); + } + }).start(); + } +} diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncLargeResponseWriterServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncLargeResponseWriterServlet.java new file mode 100644 index 0000000000..662c9dc6ce --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncLargeResponseWriterServlet.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.servlet.test.response.writer; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * Asynchronous version of {@link LargeResponseWriterServlet}. + * + * @author Flavia Rainone + */ +public class AsyncLargeResponseWriterServlet extends LargeResponseWriterServlet { + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + final AsyncContext asyncContext = req.startAsync(); + new Thread(()->{ + try { + String msg = getMessage(); + resp.setContentLength(msg.length()); + resp.getWriter().write(msg); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + asyncContext.complete(); + } + }).start(); + } +} \ No newline at end of file diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterOnPostServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterOnPostServlet.java new file mode 100644 index 0000000000..36b583b1b0 --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterOnPostServlet.java @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.servlet.test.response.writer; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * Asynchronous version of {@link ResponseWriterOnPostServlet}. + * + * @author Flavia Rainone + */ +public class AsyncResponseWriterOnPostServlet extends ResponseWriterOnPostServlet { + + @Override + protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + String test = req.getParameter("test"); + if (!test.equals(CONTENT_LENGTH_FLUSH)) { + throw new IllegalArgumentException("not a test " + test); + } + final AsyncContext asyncContext = req.startAsync(); + new Thread(()->{ + try { + HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse(); + contentLengthFlush(response); + // read after writing the response (UNDERTOW-2243) + while (true) { + if (req.getInputStream().readLine(new byte[100], 0, 100) == -1) { + req.getInputStream().close(); + break; + } + } + } catch (RuntimeException e) { + exception = e; + throw e; + } catch (Throwable t) { + exception = t; + throw new RuntimeException(t); + } finally { + asyncContext.complete(); + } + }).start(); + } +} diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterServlet.java new file mode 100644 index 0000000000..40daa50d56 --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/AsyncResponseWriterServlet.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.servlet.test.response.writer; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * Asynchronous version of {@link ResponseWriterServlet}. + * + * @author Flavia Rainone + */ +public class AsyncResponseWriterServlet extends ResponseWriterServlet { + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + + String test = req.getParameter("test"); + if (!test.equals(CONTENT_LENGTH_FLUSH)) { + throw new IllegalArgumentException("not a test " + test); + } + final AsyncContext asyncContext = req.startAsync(); + new Thread(()->{ + try { + contentLengthFlush((HttpServletResponse) asyncContext.getResponse()); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + asyncContext.complete(); + } + }).start(); + } +} diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterOnPostServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterOnPostServlet.java new file mode 100644 index 0000000000..c9c74540de --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterOnPostServlet.java @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.undertow.servlet.test.response.writer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * Servlet that writes response on post, before reading input. + * + * @author Flavia Rainone + */ +public class ResponseWriterOnPostServlet extends ResponseWriterServlet { + + protected static Throwable exception = null; + + @Override + protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + + String test = req.getParameter("test"); + if (test.equals(CONTENT_LENGTH_FLUSH)) { + contentLengthFlush(resp); + // read after writing the response (UNDERTOW-2243) + try { + while (true) { + if (req.getInputStream().readLine(new byte[100], 0, 100) == -1) { + req.getInputStream().close(); + break; + } + + } + } catch (Throwable e) { + exception = e; + throw e; + } + } else { + throw new IllegalArgumentException("not a test " + test); + } + } + + public static Throwable getExceptionIfAny() { + return exception; + } +} diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterServlet.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterServlet.java index 481b015d6e..ea832c1ab5 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterServlet.java +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterServlet.java @@ -37,13 +37,13 @@ protected void doGet(final HttpServletRequest req, final HttpServletResponse res String test = req.getParameter("test"); if (test.equals(CONTENT_LENGTH_FLUSH)) { - contentLengthFlush(req, resp); + contentLengthFlush(resp); } else { throw new IllegalArgumentException("not a test " + test); } } - private void contentLengthFlush(HttpServletRequest req, HttpServletResponse resp) throws IOException { + protected void contentLengthFlush(HttpServletResponse resp) throws IOException { int size = 10; PrintWriter pw = resp.getWriter(); @@ -54,7 +54,7 @@ private void contentLengthFlush(HttpServletRequest req, HttpServletResponse resp resp.setContentLength(size); //write more data than the content length while (i < 20) { - tmp = tmp.append("a"); + tmp.append("a"); i = i + 1; } pw.println(tmp); diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterTestCase.java index 43b535905e..eeec5713b6 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/response/writer/ResponseWriterTestCase.java @@ -18,17 +18,6 @@ package io.undertow.servlet.test.response.writer; -import jakarta.servlet.ServletException; - -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - import io.undertow.server.handlers.PathHandler; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; @@ -39,9 +28,43 @@ import io.undertow.testutils.TestHttpClient; import io.undertow.util.FileUtils; import io.undertow.util.StatusCodes; +import jakarta.servlet.ServletException; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.wildfly.common.Assert.assertTrue; /** + * Tests for response writer servlets. + * * @author Tomaz Cerar + * @author Flavia Rainone */ @RunWith(DefaultServer.class) public class ResponseWriterTestCase { @@ -58,10 +81,24 @@ public static void setup() throws ServletException { .setDeploymentName("servletContext.war") .addServlet(Servlets.servlet("resp", ResponseWriterServlet.class) .addMapping("/resp")) - .addServlet(Servlets.servlet("respLArget", LargeResponseWriterServlet.class) + .addServlet(Servlets.servlet("respLarge", LargeResponseWriterServlet.class) .addMapping("/large")) .addServlet(Servlets.servlet("exception", ExceptionWriterServlet.class) - .addMapping("/exception")); + .addMapping("/exception")) + .addServlet(Servlets.servlet("respBeforeRead", ResponseWriterOnPostServlet.class) + .addMapping("/resp-before-read")) + .addServlet(Servlets.servlet("asyncResp", AsyncResponseWriterServlet.class) + .setAsyncSupported(true) + .addMapping("/async-resp")) + .addServlet(Servlets.servlet("asyncRespLarge", AsyncLargeResponseWriterServlet.class) + .setAsyncSupported(true) + .addMapping("/async-large")) + .addServlet(Servlets.servlet("asyncException", AsyncExceptionWriterServlet.class) + .setAsyncSupported(true) + .addMapping("/async-exception")) + .addServlet(Servlets.servlet("asyncRespBeforeRead", AsyncResponseWriterOnPostServlet.class) + .setAsyncSupported(true) + .addMapping("/async-resp-before-read")); DeploymentManager manager = container.addDeployment(builder); manager.deploy(); @@ -71,30 +108,47 @@ public static void setup() throws ServletException { @Test public void testContentLengthBasedFlush() throws Exception { + assertContentLengthBasedFlush("resp"); + } + + @Test + public void testAsyncContentLengthBasedFlush() throws Exception { + assertContentLengthBasedFlush("async-resp"); + } + + private void assertContentLengthBasedFlush(String path) throws Exception { TestHttpClient client = new TestHttpClient(); try { - HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/resp?test=" + ResponseWriterServlet.CONTENT_LENGTH_FLUSH); + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/" + path + "?test=" + ResponseWriterServlet.CONTENT_LENGTH_FLUSH); HttpResponse result = client.execute(get); - Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); String data = FileUtils.readFile(result.getEntity().getContent()); - Assert.assertEquals("first-aaaa", data); - Assert.assertEquals(0, result.getHeaders("not-header").length); + assertEquals("first-aaaa", data); + assertEquals(0, result.getHeaders("not-header").length); } finally { client.getConnectionManager().shutdown(); } } - @Test public void testWriterLargeResponse() throws Exception { + assertWriterLargeResponse("large"); + } + + @Test + public void testAsyncWriterLargeResponse() throws Exception { + assertWriterLargeResponse("async-large"); + } + + private void assertWriterLargeResponse(String path) throws Exception { TestHttpClient client = new TestHttpClient(); try { - HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/large"); + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/" + path); HttpResponse result = client.execute(get); - Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); String data = FileUtils.readFile(result.getEntity().getContent()); - Assert.assertEquals(LargeResponseWriterServlet.getMessage(), data); + assertEquals(LargeResponseWriterServlet.getMessage(), data); } finally { client.getConnectionManager().shutdown(); @@ -103,15 +157,92 @@ public void testWriterLargeResponse() throws Exception { @Test public void testExceptionResponse() throws Exception { + assertExceptionResponse("exception"); + } + + @Test + public void testAsyncExceptionResponse() throws Exception { + assertExceptionResponse("async-exception"); + } + + private void assertExceptionResponse(String path) throws Exception { TestHttpClient client = new TestHttpClient(); try { - HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/exception"); + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/" + path); HttpResponse result = client.execute(get); - Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); String response = FileUtils.readFile(result.getEntity().getContent()); MatcherAssert.assertThat(response, CoreMatchers.startsWith("java.lang.Exception: TestException")); } finally { client.getConnectionManager().shutdown(); } } + + @Test + public void testRespondBeforeRead() throws Throwable { + assertRespondBeforeRead("resp-before-read"); + } + + @Test + public void testAsyncRespondBeforeRead() throws Throwable { + assertRespondBeforeRead("async-resp-before-read"); + } + + private void assertRespondBeforeRead(String path) throws Throwable { + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader(HttpHeaders.CONNECTION, "close")); + final HttpClientBuilder builder = HttpClients.custom().setDefaultHeaders(headers) + .setConnectionReuseStrategy(new NoConnectionReuseStrategy()); + try (CloseableHttpClient client = builder.build()) { + final HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/servletContext/" + path + "?test=" + ResponseWriterServlet.CONTENT_LENGTH_FLUSH); + // anything will do, send the bytecodes of this class just for testing purposes + final Path rootPath = Paths.get(getClass().getResource(getClass().getSimpleName() + ".class").toURI()); + final SlowInputStream inputStream = new SlowInputStream(new BufferedInputStream(new FileInputStream(rootPath.toFile()))); + post.setEntity(new InputStreamEntity(inputStream)); + final HttpResponse result = client.execute(post); + // wait til it is fully read + boolean fullyRead = inputStream.waitTillIsFullyRead(); + // check if servlet ran without any exceptions + final Throwable exception = ResponseWriterOnPostServlet.getExceptionIfAny(); + if (exception != null) { + throw exception; + } + assertTrue(fullyRead); + assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String data = FileUtils.readFile(result.getEntity().getContent()); + assertEquals("first-aaaa", data); + assertEquals(0, result.getHeaders("not-header").length); + } + } + + private static class SlowInputStream extends InputStream { + private final InputStream innerInputStream; + final CountDownLatch latch = new CountDownLatch(1); + + SlowInputStream(InputStream innerInputStream) { + this.innerInputStream = innerInputStream; + } + + @Override // enforce that reading will take place after the response is written with some extra delay + public int read() throws IOException { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + int readByte = innerInputStream.read(); + if (readByte == -1) { + latch.countDown(); + } + return readByte; + } + + boolean waitTillIsFullyRead() throws InterruptedException { + boolean fullyRead = latch.await(60, TimeUnit.SECONDS); + // I tested this and the extra sleep time is necessary to be able to view any exception caught + // by the servlet, since the exception might happen after read above has returned -1 + Thread.sleep(50); + return fullyRead; + } + } }