| /* | |
| * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.net.InetAddress; | |
| import java.net.ProtocolException; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpTimeoutException; | |
| import java.nio.charset.StandardCharsets; | |
| import java.time.Duration; | |
| import javax.net.ssl.SSLContext; | |
| import jdk.httpclient.test.lib.common.HttpServerAdapters; | |
| import jdk.httpclient.test.lib.http2.Http2TestServer; | |
| import jdk.test.lib.net.SimpleSSLContext; | |
| import jdk.test.lib.net.URIBuilder; | |
| import org.testng.Assert; | |
| import org.testng.annotations.AfterClass; | |
| import org.testng.annotations.BeforeClass; | |
| import org.testng.annotations.Test; | |
| import static java.net.http.HttpClient.Version.HTTP_2; | |
| /** | |
| * @test | |
| * @bug 8292044 | |
| * @summary Tests behaviour of HttpClient when server responds with 102 or 103 status codes | |
| * @library /test/lib /test/jdk/java/net/httpclient/lib | |
| * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters | |
| * jdk.httpclient.test.lib.http2.Http2TestServer | |
| * @run testng/othervm -Djdk.internal.httpclient.debug=true | |
| * * -Djdk.httpclient.HttpClient.log=headers,requests,responses,errors Response1xxTest | |
| */ | |
| public class Response1xxTest implements HttpServerAdapters { | |
| private static final String EXPECTED_RSP_BODY = "Hello World"; | |
| private ServerSocket serverSocket; | |
| private Http11Server server; | |
| private String http1RequestURIBase; | |
| private HttpTestServer http2Server; // h2c | |
| private String http2RequestURIBase; | |
| private SSLContext sslContext; | |
| private HttpTestServer https2Server; // h2 | |
| private String https2RequestURIBase; | |
| private final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; | |
| @BeforeClass | |
| public void setup() throws Exception { | |
| serverSocket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); | |
| server = new Http11Server(serverSocket); | |
| new Thread(server).start(); | |
| http1RequestURIBase = URIBuilder.newBuilder().scheme("http").loopback() | |
| .port(serverSocket.getLocalPort()).build().toString(); | |
| http2Server = HttpTestServer.create(HTTP_2); | |
| http2Server.addHandler(new Http2Handler(), "/http2/102"); | |
| http2Server.addHandler(new Http2Handler(), "/http2/103"); | |
| http2Server.addHandler(new Http2Handler(), "/http2/100"); | |
| http2Server.addHandler(new Http2Handler(), "/http2/101"); | |
| http2Server.addHandler(new OKHandler(), "/http2/200"); | |
| http2Server.addHandler(new OnlyInformationalHandler(), "/http2/only-informational"); | |
| http2RequestURIBase = URIBuilder.newBuilder().scheme("http").loopback() | |
| .port(http2Server.getAddress().getPort()) | |
| .path("/http2").build().toString(); | |
| http2Server.start(); | |
| System.out.println("Started HTTP2 server at " + http2Server.getAddress()); | |
| sslContext = new SimpleSSLContext().get(); | |
| if (sslContext == null) { | |
| throw new AssertionError("Unexpected null sslContext"); | |
| } | |
| https2Server = HttpTestServer.create(HTTP_2, sslContext); | |
| https2Server.addHandler(new Http2Handler(), "/http2/101"); | |
| https2RequestURIBase = URIBuilder.newBuilder().scheme("https").loopback() | |
| .port(https2Server.getAddress().getPort()) | |
| .path("/http2").build().toString(); | |
| https2Server.start(); | |
| System.out.println("Started (https) HTTP2 server at " + https2Server.getAddress()); | |
| } | |
| @AfterClass | |
| public void teardown() throws Throwable { | |
| try { | |
| assertNoOutstandingClientOps(); | |
| } finally { | |
| if (server != null) { | |
| server.stop = true; | |
| System.out.println("(HTTP 1.1) Server stop requested"); | |
| } | |
| if (serverSocket != null) { | |
| serverSocket.close(); | |
| System.out.println("Closed (HTTP 1.1) server socket"); | |
| } | |
| if (http2Server != null) { | |
| http2Server.stop(); | |
| System.out.println("Stopped HTTP2 server"); | |
| } | |
| if (https2Server != null) { | |
| https2Server.stop(); | |
| System.out.println("Stopped (https) HTTP2 server"); | |
| } | |
| } | |
| } | |
| private static final class Http11Server implements Runnable { | |
| private static final int CONTENT_LENGTH = EXPECTED_RSP_BODY.getBytes(StandardCharsets.UTF_8).length; | |
| private static final String HTTP_1_1_RSP_200 = "HTTP/1.1 200 OK\r\n" + | |
| "Content-Length: " + CONTENT_LENGTH + "\r\n\r\n" + | |
| EXPECTED_RSP_BODY; | |
| private static final String REQ_LINE_FOO = "GET /test/foo HTTP/1.1\r\n"; | |
| private static final String REQ_LINE_BAR = "GET /test/bar HTTP/1.1\r\n"; | |
| private static final String REQ_LINE_HELLO = "GET /test/hello HTTP/1.1\r\n"; | |
| private static final String REQ_LINE_BYE = "GET /test/bye HTTP/1.1\r\n"; | |
| private final ServerSocket serverSocket; | |
| private volatile boolean stop; | |
| private Http11Server(final ServerSocket serverSocket) { | |
| this.serverSocket = serverSocket; | |
| } | |
| @Override | |
| public void run() { | |
| System.out.println("Server running at " + serverSocket); | |
| while (!stop) { | |
| Socket socket = null; | |
| try { | |
| // accept a connection | |
| socket = serverSocket.accept(); | |
| System.out.println("Accepted connection from client " + socket); | |
| // read request | |
| final String requestLine; | |
| try { | |
| requestLine = readRequestLine(socket); | |
| } catch (Throwable t) { | |
| // ignore connections from potential rogue client | |
| System.err.println("Ignoring connection/request from client " + socket | |
| + " due to exception:"); | |
| t.printStackTrace(); | |
| // close the socket | |
| safeClose(socket); | |
| continue; | |
| } | |
| System.out.println("Received following request line from client " + socket | |
| + " :\n" + requestLine); | |
| final int informationalResponseCode; | |
| if (requestLine.startsWith(REQ_LINE_FOO)) { | |
| // we will send intermediate/informational 102 response | |
| informationalResponseCode = 102; | |
| } else if (requestLine.startsWith(REQ_LINE_BAR)) { | |
| // we will send intermediate/informational 103 response | |
| informationalResponseCode = 103; | |
| } else if (requestLine.startsWith(REQ_LINE_HELLO)) { | |
| // we will send intermediate/informational 100 response | |
| informationalResponseCode = 100; | |
| } else if (requestLine.startsWith(REQ_LINE_BYE)) { | |
| // we will send intermediate/informational 101 response | |
| informationalResponseCode = 101; | |
| } else { | |
| // unexpected client. ignore and close the client | |
| System.err.println("Ignoring unexpected request from client " + socket); | |
| safeClose(socket); | |
| continue; | |
| } | |
| try (final OutputStream os = socket.getOutputStream()) { | |
| // send informational response headers a few times (spec allows them to | |
| // be sent multiple times) | |
| for (int i = 0; i < 3; i++) { | |
| // send 1xx response header | |
| if (informationalResponseCode == 101) { | |
| os.write(("HTTP/1.1 " + informationalResponseCode + "\r\n" + | |
| "Connection: upgrade\r\n" + | |
| "Upgrade: websocket\r\n\r\n") | |
| .getBytes(StandardCharsets.UTF_8)); | |
| } else { | |
| os.write(("HTTP/1.1 " + informationalResponseCode + "\r\n\r\n") | |
| .getBytes(StandardCharsets.UTF_8)); | |
| } | |
| os.flush(); | |
| System.out.println("Sent response code " + informationalResponseCode | |
| + " to client " + socket); | |
| } | |
| // now send a final response | |
| System.out.println("Now sending 200 response code to client " + socket); | |
| os.write(HTTP_1_1_RSP_200.getBytes(StandardCharsets.UTF_8)); | |
| os.flush(); | |
| System.out.println("Sent 200 response code to client " + socket); | |
| } | |
| } catch (Throwable t) { | |
| // close the client connection | |
| safeClose(socket); | |
| // continue accepting any other client connections until we are asked to stop | |
| System.err.println("Ignoring exception in server:"); | |
| t.printStackTrace(); | |
| } | |
| } | |
| } | |
| static String readRequestLine(final Socket sock) throws IOException { | |
| final InputStream is = sock.getInputStream(); | |
| final StringBuilder sb = new StringBuilder(""); | |
| byte[] buf = new byte[1024]; | |
| while (!sb.toString().endsWith("\r\n\r\n")) { | |
| final int numRead = is.read(buf); | |
| if (numRead == -1) { | |
| return sb.toString(); | |
| } | |
| final String part = new String(buf, 0, numRead, StandardCharsets.ISO_8859_1); | |
| sb.append(part); | |
| } | |
| return sb.toString(); | |
| } | |
| private static void safeClose(final Socket socket) { | |
| try { | |
| socket.close(); | |
| } catch (Throwable t) { | |
| // ignore | |
| } | |
| } | |
| } | |
| private static class Http2Handler implements HttpTestHandler { | |
| @Override | |
| public void handle(final HttpTestExchange exchange) throws IOException { | |
| final URI requestURI = exchange.getRequestURI(); | |
| final int informationResponseCode; | |
| if (requestURI.getPath().endsWith("/102")) { | |
| informationResponseCode = 102; | |
| } else if (requestURI.getPath().endsWith("/103")) { | |
| informationResponseCode = 103; | |
| } else if (requestURI.getPath().endsWith("/100")) { | |
| informationResponseCode = 100; | |
| } else if (requestURI.getPath().endsWith("/101")) { | |
| informationResponseCode = 101; | |
| } else { | |
| // unexpected request | |
| System.err.println("Unexpected request " + requestURI + " from client " | |
| + exchange.getRemoteAddress()); | |
| exchange.sendResponseHeaders(400, -1); | |
| return; | |
| } | |
| // send informational response headers a few times (spec allows them to | |
| // be sent multiple times) | |
| for (int i = 0; i < 3; i++) { | |
| exchange.sendResponseHeaders(informationResponseCode, -1); | |
| System.out.println("Sent " + informationResponseCode + " response code from H2 server"); | |
| } | |
| // now send 200 response | |
| try { | |
| final byte[] body = EXPECTED_RSP_BODY.getBytes(StandardCharsets.UTF_8); | |
| exchange.sendResponseHeaders(200, body.length); | |
| System.out.println("Sent 200 response from H2 server"); | |
| try (OutputStream os = exchange.getResponseBody()) { | |
| os.write(body); | |
| } | |
| System.out.println("Sent response body from H2 server"); | |
| } catch (Throwable e) { | |
| System.err.println("Failed to send response from HTTP2 handler:"); | |
| e.printStackTrace(); | |
| throw e; | |
| } | |
| } | |
| } | |
| private static class OnlyInformationalHandler implements HttpTestHandler { | |
| @Override | |
| public void handle(final HttpTestExchange exchange) throws IOException { | |
| // we only send informational response and then return | |
| for (int i = 0; i < 5; i++) { | |
| exchange.sendResponseHeaders(102, -1); | |
| System.out.println("Sent 102 response code from H2 server"); | |
| // wait for a while before sending again | |
| try { | |
| Thread.sleep(2000); | |
| } catch (InterruptedException e) { | |
| // just return | |
| System.err.println("Handler thread interrupted"); | |
| } | |
| } | |
| } | |
| } | |
| private static class OKHandler implements HttpTestHandler { | |
| @Override | |
| public void handle(final HttpTestExchange exchange) throws IOException { | |
| exchange.sendResponseHeaders(200, -1); | |
| } | |
| } | |
| /** | |
| * Tests that when a HTTP/1.1 server sends intermediate 1xx response codes and then the final | |
| * response, the client (internally) will ignore those intermediate informational response codes | |
| * and only return the final response to the application | |
| */ | |
| @Test | |
| public void test1xxForHTTP11() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HttpClient.Version.HTTP_1_1) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| final URI[] requestURIs = new URI[]{ | |
| new URI(http1RequestURIBase + "/test/foo"), | |
| new URI(http1RequestURIBase + "/test/bar"), | |
| new URI(http1RequestURIBase + "/test/hello")}; | |
| for (final URI requestURI : requestURIs) { | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| final HttpResponse<String> response = client.send(request, | |
| HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); | |
| Assert.assertEquals(response.version(), HttpClient.Version.HTTP_1_1, | |
| "Unexpected HTTP version in response"); | |
| Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); | |
| Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); | |
| } | |
| } | |
| /** | |
| * Tests that when a HTTP2 server sends intermediate 1xx response codes and then the final | |
| * response, the client (internally) will ignore those intermediate informational response codes | |
| * and only return the final response to the application | |
| */ | |
| @Test | |
| public void test1xxForHTTP2() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HTTP_2) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| final URI[] requestURIs = new URI[]{ | |
| new URI(http2RequestURIBase + "/102"), | |
| new URI(http2RequestURIBase + "/103"), | |
| new URI(http2RequestURIBase + "/100")}; | |
| for (final URI requestURI : requestURIs) { | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| final HttpResponse<String> response = client.send(request, | |
| HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); | |
| Assert.assertEquals(response.version(), HTTP_2, | |
| "Unexpected HTTP version in response"); | |
| Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); | |
| Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); | |
| } | |
| } | |
| /** | |
| * Tests that when a request is issued with a specific request timeout and the server | |
| * responds with intermediate 1xx response code but doesn't respond with a final response within | |
| * the timeout duration, then the application fails with a request timeout | |
| */ | |
| @Test | |
| public void test1xxRequestTimeout() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HTTP_2) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| final URI requestURI = new URI(http2RequestURIBase + "/only-informational"); | |
| final Duration requestTimeout = Duration.ofSeconds(2); | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).timeout(requestTimeout) | |
| .build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| // we expect the request to timeout | |
| Assert.assertThrows(HttpTimeoutException.class, () -> { | |
| client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); | |
| }); | |
| } | |
| /** | |
| * Tests that when the HTTP/1.1 server sends a 101 response when the request hasn't asked | |
| * for an "Upgrade" then the request fails. | |
| */ | |
| @Test | |
| public void testHTTP11Unexpected101() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HttpClient.Version.HTTP_1_1) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| final URI requestURI = new URI(http1RequestURIBase + "/test/bye"); | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| // we expect the request to fail because the server sent an unexpected 101 | |
| Assert.assertThrows(ProtocolException.class, | |
| () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); | |
| } | |
| /** | |
| * Tests that when the HTTP2 server (over HTTPS) sends a 101 response when the request | |
| * hasn't asked for an "Upgrade" then the request fails. | |
| */ | |
| @Test | |
| public void testSecureHTTP2Unexpected101() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HTTP_2) | |
| .sslContext(sslContext) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| final URI requestURI = new URI(https2RequestURIBase + "/101"); | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| // we expect the request to fail because the server sent an unexpected 101 | |
| Assert.assertThrows(ProtocolException.class, | |
| () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); | |
| } | |
| /** | |
| * Tests that when the HTTP2 server (over plain HTTP) sends a 101 response when the request | |
| * hasn't asked for an "Upgrade" then the request fails. | |
| */ | |
| @Test | |
| public void testPlainHTTP2Unexpected101() throws Exception { | |
| final HttpClient client = HttpClient.newBuilder() | |
| .version(HTTP_2) | |
| .proxy(HttpClient.Builder.NO_PROXY).build(); | |
| TRACKER.track(client); | |
| // when using HTTP2 version against a "http://" (non-secure) URI | |
| // the HTTP client (implementation) internally initiates a HTTP/1.1 connection | |
| // and then does an "Upgrade:" to "h2c". This it does when there isn't already a | |
| // H2 connection against the target/destination server. So here we initiate a dummy request | |
| // using the client instance against the same target server and just expect it to return | |
| // back successfully. Once that connection is established (and internally pooled), the client | |
| // will then reuse that connection and won't issue an "Upgrade:" and thus we can then | |
| // start our testing | |
| warmupH2Client(client); | |
| // start the actual testing | |
| final URI requestURI = new URI(http2RequestURIBase + "/101"); | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing request to " + requestURI); | |
| // we expect the request to fail because the server sent an unexpected 101 | |
| Assert.assertThrows(ProtocolException.class, | |
| () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); | |
| } | |
| // sends a request and expects a 200 response back | |
| private void warmupH2Client(final HttpClient client) throws Exception { | |
| final URI requestURI = new URI(http2RequestURIBase + "/200"); | |
| final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); | |
| System.out.println("Issuing (warmup) request to " + requestURI); | |
| final HttpResponse<Void> response = client.send(request, HttpResponse.BodyHandlers.discarding()); | |
| Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); | |
| } | |
| // verifies that the HttpClient being tracked has no outstanding operations | |
| private void assertNoOutstandingClientOps() throws AssertionError { | |
| System.gc(); | |
| final AssertionError refCheckFailure = TRACKER.check(1000); | |
| if (refCheckFailure != null) { | |
| throw refCheckFailure; | |
| } | |
| // successful test completion | |
| } | |
| } |