diff --git a/jetty-ee9/jetty-ee9-proxy/pom.xml b/jetty-ee9/jetty-ee9-proxy/pom.xml index 255c1fe45a99..7c5144686569 100644 --- a/jetty-ee9/jetty-ee9-proxy/pom.xml +++ b/jetty-ee9/jetty-ee9-proxy/pom.xml @@ -38,6 +38,12 @@ jetty-jakarta-servlet-api provided + + org.apache.httpcomponents.client5 + httpclient5 + 5.4-beta1 + test + org.eclipse.jetty jetty-http-tools diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java index 064ac6687455..af5c7280be20 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java @@ -13,6 +13,13 @@ package org.eclipse.jetty.ee9.proxy; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -41,18 +48,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletInputStream; -import jakarta.servlet.ServletOutputStream; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.AsyncRequestContent; import org.eclipse.jetty.client.BytesRequestContent; import org.eclipse.jetty.client.CompletableResponseListener; @@ -88,12 +90,25 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.util.Timeout; @ExtendWith(WorkDirExtension.class) public class AsyncMiddleManServletTest @@ -170,6 +185,88 @@ public void dispose() throws Exception LifeCycle.stop(proxy); } + /** + * Tests https://github.com/jetty/jetty.project/issues/11841 + * + * @throws Exception + */ + @Test + public void testServletExpect100ApacheHttpClient5() throws Exception + { + startServer(new HttpServlet() + { + private static final long serialVersionUID = 1L; + private AtomicInteger count = new AtomicInteger(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + // Adding logging may slow things down enough to not hang + // System.out.printf("Origin servicing %d%n", count.incrementAndGet()); + // Send the 100 Continue. + ServletInputStream input = request.getInputStream(); + // Echo the content. + IO.copy(input, response.getOutputStream()); + // Slowing down the origin allows the test to not hang sometimes +// try { +// Thread.sleep(50); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + } + }); + startProxy(new AsyncMiddleManServlet() + { + @Override + protected void onProxyResponseFailure(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Response serverResponse, + Throwable failure) { + if (failure != null) { + // This test shows the NPE on the console: + // Sometimes Jetty recovers from the NPE and replies with a 200. + // Sometimes Jetty doesn't recovers from the NPE and replies with a 502. + failure.printStackTrace(); + } + super.onProxyResponseFailure(clientRequest, proxyResponse, serverResponse, failure); + } + + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + // Adding logging may slow things down enough to not hang + // System.out.printf("AsyncMiddleManServlet... %n"); + super.service(request, response); + } + }); + + RequestConfig requestConfig = RequestConfig.custom() + .setExpectContinueEnabled(true) + .setResponseTimeout(5, TimeUnit.SECONDS) + .build(); + HttpHost proxyHost = new HttpHost("localhost", proxyConnector.getLocalPort()); + HttpHost targetHost = new HttpHost("localhost", serverConnector.getLocalPort()); + HttpRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyHost); + try (CloseableHttpClient httpclient = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setRoutePlanner(routePlanner) + .build()) { + // loop to attempt increase odds of hanging + for (int i = 1; i <= 500_000; i++) { + long start = System.currentTimeMillis(); + ClassicHttpRequest request = ClassicRequestBuilder.post("http://localhost:" + serverConnector.getLocalPort()) + .setHeader(HttpHeader.EXPECT.toString(), HttpHeaderValue.CONTINUE.toString()) + .setEntity("a") + .build(); + httpclient.execute(targetHost, request, response -> { + // System.out.println(response.getCode() + " " + response.getReasonPhrase()); + assertEquals(200, response.getCode()); + HttpEntity entity = response.getEntity(); + EntityUtils.consume(entity); + return null; + }); + System.out.printf("# %,d %,d ms%n", i, System.currentTimeMillis() - start); + } + } + } + @Test public void testNewDestroy() throws Exception {