diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index d08d13b15ce..75bf4a53ea0 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -51,6 +51,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res private RetainableByteBuffer networkBuffer; private boolean shutdown; private boolean complete; + private boolean unsolicited; public HttpReceiverOverHTTP(HttpChannelOverHTTP channel) { @@ -272,6 +273,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public void startResponse(HttpVersion version, int status, String reason) { HttpExchange exchange = getHttpExchange(); + unsolicited = exchange == null; if (exchange == null) return; @@ -287,7 +289,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public void parsedHeader(HttpField field) { HttpExchange exchange = getHttpExchange(); - if (exchange == null) + unsolicited |= exchange == null; + if (unsolicited) return; responseHeader(exchange, field); @@ -297,7 +300,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public boolean headerComplete() { HttpExchange exchange = getHttpExchange(); - if (exchange == null) + unsolicited |= exchange == null; + if (unsolicited) return false; // Store the EndPoint is case of upgrades, tunnels, etc. @@ -309,7 +313,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public boolean content(ByteBuffer buffer) { HttpExchange exchange = getHttpExchange(); - if (exchange == null) + unsolicited |= exchange == null; + if (unsolicited) return false; RetainableByteBuffer networkBuffer = this.networkBuffer; @@ -331,7 +336,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public void parsedTrailer(HttpField trailer) { HttpExchange exchange = getHttpExchange(); - if (exchange == null) + unsolicited |= exchange == null; + if (unsolicited) return; exchange.getResponse().trailer(trailer); @@ -341,8 +347,12 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public boolean messageComplete() { HttpExchange exchange = getHttpExchange(); - if (exchange == null) + if (exchange == null || unsolicited) + { + // We received an unsolicited response from the server. + getHttpConnection().close(); return false; + } int status = exchange.getResponse().getStatus(); if (status != HttpStatus.CONTINUE_100) @@ -359,7 +369,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res { HttpExchange exchange = getHttpExchange(); HttpConnectionOverHTTP connection = getHttpConnection(); - if (exchange == null) + if (exchange == null || unsolicited) connection.close(); else failAndClose(new EOFException(String.valueOf(connection))); @@ -369,7 +379,11 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res public void badMessage(BadMessageException failure) { HttpExchange exchange = getHttpExchange(); - if (exchange != null) + if (exchange == null || unsolicited) + { + getHttpConnection().close(); + } + else { HttpResponse response = exchange.getResponse(); response.status(failure.getCode()).reason(failure.getReason()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index 850eb5152f2..2297b560ad9 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -1820,6 +1820,65 @@ public class HttpClientTest extends AbstractHttpClientServerTest assertArrayEquals(bytes, baos.toByteArray()); } + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testUnsolicitedResponseBytesFromServer(Scenario scenario) throws Exception + { + String response = "" + + "HTTP/1.1 408 Request Timeout\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"; + testUnsolicitedBytesFromServer(scenario, response); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testUnsolicitedInvalidBytesFromServer(Scenario scenario) throws Exception + { + String response = "ABCDEF"; + testUnsolicitedBytesFromServer(scenario, response); + } + + private void testUnsolicitedBytesFromServer(Scenario scenario, String bytesFromServer) throws Exception + { + try (ServerSocket server = new ServerSocket(0)) + { + startClient(scenario, clientConnector -> + { + clientConnector.setSelectors(1); + HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(clientConnector); + transport.setConnectionPoolFactory(destination -> + { + ConnectionPool connectionPool = new DuplexConnectionPool(destination, 1, destination); + connectionPool.preCreateConnections(1); + return connectionPool; + }); + return transport; + }, null); + + String host = "localhost"; + int port = server.getLocalPort(); + + // Resolve the destination which will pre-create a connection. + HttpDestination destination = client.resolveDestination(new Origin("http", host, port)); + + // Accept the connection and send an unsolicited 408. + try (Socket socket = server.accept()) + { + OutputStream output = socket.getOutputStream(); + output.write(bytesFromServer.getBytes(StandardCharsets.UTF_8)); + output.flush(); + } + + // Give some time to the client to process the response. + Thread.sleep(1000); + + AbstractConnectionPool pool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(0, pool.getConnectionCount()); + } + } + private void assertCopyRequest(Request original) { Request copy = client.copyRequest((HttpRequest)original, original.getURI());