| /* | |
| * Copyright (c) 2022, 2024, 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. | |
| */ | |
| /* | |
| * @test | |
| * @summary Tests basic handling of Partial Responses by the HttpClient | |
| * @bug 8286171 8307648 | |
| * @library /test/lib /test/jdk/java/net/httpclient/lib | |
| * @build jdk.httpclient.test.lib.common.HttpServerAdapters | |
| * @run testng/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors ExpectContinueTest | |
| */ | |
| import jdk.httpclient.test.lib.http2.BodyOutputStream; | |
| import jdk.httpclient.test.lib.http2.Http2Handler; | |
| import jdk.httpclient.test.lib.http2.Http2TestExchange; | |
| import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; | |
| import jdk.httpclient.test.lib.http2.Http2TestServer; | |
| import jdk.httpclient.test.lib.http2.Http2TestServerConnection; | |
| import jdk.internal.net.http.common.HttpHeadersBuilder; | |
| import jdk.internal.net.http.frame.HeaderFrame; | |
| import org.testng.TestException; | |
| import org.testng.annotations.AfterTest; | |
| import org.testng.annotations.BeforeTest; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.Test; | |
| import javax.net.ServerSocketFactory; | |
| import javax.net.ssl.SSLSession; | |
| import java.io.BufferedReader; | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStream; | |
| import java.io.OutputStreamWriter; | |
| import java.io.PrintStream; | |
| import java.io.PrintWriter; | |
| import java.io.Writer; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| 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.HttpClient.Builder; | |
| import java.net.http.HttpHeaders; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.util.StringTokenizer; | |
| import java.util.concurrent.CancellationException; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ExecutionException; | |
| import jdk.httpclient.test.lib.common.HttpServerAdapters; | |
| import static java.net.http.HttpClient.Version.HTTP_1_1; | |
| import static java.net.http.HttpClient.Version.HTTP_2; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.testng.Assert.*; | |
| public class ExpectContinueTest implements HttpServerAdapters { | |
| HttpTestServer http1TestServer; // HTTP/1.1 | |
| Http1HangServer http1HangServer; | |
| Http2TestServer http2TestServer; // HTTP/2 | |
| URI getUri, postUri, forcePostUri, hangUri; | |
| URI h2postUri, h2forcePostUri, h2hangUri, h2endStreamUri, h2warmupURI; | |
| static PrintStream err = new PrintStream(System.err); | |
| static PrintStream out = new PrintStream(System.out); | |
| static final String EXPECTATION_FAILED_417 = "417 Expectation Failed"; | |
| @DataProvider(name = "uris") | |
| public Object[][] urisData() { | |
| return new Object[][]{ | |
| // URI, Expected Status Code, Will finish with Exception, Protocol Version | |
| { postUri, 200, false, HTTP_1_1 }, | |
| { forcePostUri, 200, false, HTTP_1_1 }, | |
| { hangUri, 417, false, HTTP_1_1}, | |
| { h2postUri, 200, false, HTTP_2 }, | |
| { h2forcePostUri, 200, false, HTTP_2 }, | |
| { h2hangUri, 417, false, HTTP_2 }, | |
| { h2endStreamUri, 200, true, HTTP_2 }, // Error | |
| }; | |
| } | |
| @Test(dataProvider = "uris") | |
| public void test(URI uri, int expectedStatusCode, boolean exceptionally, HttpClient.Version version) | |
| throws CancellationException, InterruptedException, ExecutionException, IOException { | |
| err.printf("\nTesting with Version: %s, URI: %s, exceptionally: %b\n", version, uri, exceptionally); | |
| try (HttpClient client = HttpClient.newBuilder().proxy(Builder.NO_PROXY).version(version).build()) { | |
| HttpResponse<String> resp = null; | |
| Throwable testThrowable = null; | |
| if (!version.equals(HTTP_1_1)) { | |
| err.printf("Performing warmup request to %s", h2warmupURI); | |
| client.send(HttpRequest.newBuilder(h2warmupURI).GET().version(version).build(), HttpResponse.BodyHandlers.discarding()); | |
| } | |
| HttpRequest postRequest = HttpRequest.newBuilder(uri) | |
| .version(version) | |
| .POST(HttpRequest.BodyPublishers.ofString("Sample Post")) | |
| .expectContinue(true) | |
| .build(); | |
| err.printf("Sending request (%s): %s%n", version, postRequest); | |
| CompletableFuture<HttpResponse<String>> cf = client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString()); | |
| try { | |
| resp = cf.get(); | |
| } catch (Exception e) { | |
| testThrowable = e.getCause(); | |
| } | |
| verifyRequest(uri.getPath(), expectedStatusCode, resp, exceptionally, testThrowable); | |
| } | |
| } | |
| @BeforeTest | |
| public void setup() throws Exception { | |
| InetSocketAddress saHang = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); | |
| http1TestServer = HttpTestServer.create(HTTP_1_1); | |
| http1TestServer.addHandler(new GetHandler(), "/http1/get"); | |
| http1TestServer.addHandler(new PostHandler(), "/http1/post"); | |
| http1TestServer.addHandler(new ForcePostHandler(), "/http1/forcePost"); | |
| getUri = URI.create("http://" + http1TestServer.serverAuthority() + "/http1/get"); | |
| postUri = URI.create("http://" + http1TestServer.serverAuthority() + "/http1/post"); | |
| forcePostUri = URI.create("http://" + http1TestServer.serverAuthority() + "/http1/forcePost"); | |
| // Due to limitations of the above Http1 Test Server, a manual approach is taken to test the hanging with the | |
| // httpclient using Http1 so that the correct response header can be returned for the test case | |
| http1HangServer = new Http1HangServer(saHang); | |
| hangUri = URI.create("http://" + http1HangServer.ia.getCanonicalHostName() + ":" + http1HangServer.port + "/http1/hang"); | |
| http2TestServer = new Http2TestServer(false, 0); | |
| http2TestServer.setExchangeSupplier(ExpectContinueTestExchangeImpl::new); | |
| http2TestServer.addHandler(new GetHandler().toHttp2Handler(), "/http2/warmup"); | |
| http2TestServer.addHandler(new PostHandler().toHttp2Handler(), "/http2/post"); | |
| http2TestServer.addHandler(new ForcePostHandler().toHttp2Handler(), "/http2/forcePost"); | |
| http2TestServer.addHandler(new PostHandlerCantContinue().toHttp2Handler(), "/http2/hang"); | |
| http2TestServer.addHandler(new PostHandlerHttp2(), "/http2/endStream"); | |
| h2warmupURI = new URI("http://" + http2TestServer.serverAuthority() + "/http2/warmup"); | |
| h2postUri = URI.create("http://" + http2TestServer.serverAuthority() + "/http2/post"); | |
| h2forcePostUri = URI.create("http://" + http2TestServer.serverAuthority() + "/http2/forcePost"); | |
| h2hangUri = URI.create("http://" + http2TestServer.serverAuthority() + "/http2/hang"); | |
| h2endStreamUri = URI.create("http://" + http2TestServer.serverAuthority() + "/http2/endStream"); | |
| out.printf("HTTP/1.1 server listening at: %s %n", http1TestServer.serverAuthority()); | |
| out.printf("HTTP/1.1 hang server listening at: %s %n", hangUri.getRawAuthority()); | |
| out.printf("HTTP/2 clear server listening at: %s %n", http2TestServer.serverAuthority()); | |
| http1TestServer.start(); | |
| http1HangServer.start(); | |
| http2TestServer.start(); | |
| } | |
| @AfterTest | |
| public void teardown() throws IOException { | |
| http1TestServer.stop(); | |
| http1HangServer.close(); | |
| http2TestServer.stop(); | |
| } | |
| static class GetHandler implements HttpTestHandler { | |
| @Override | |
| public void handle(HttpTestExchange exchange) throws IOException { | |
| try (OutputStream os = exchange.getResponseBody()) { | |
| byte[] bytes = "Response Body".getBytes(UTF_8); | |
| err.printf("Server sending 200 (length=%s)", bytes.length); | |
| exchange.sendResponseHeaders(200, bytes.length); | |
| err.println("Server sending Response Body"); | |
| os.write(bytes); | |
| } | |
| } | |
| } | |
| static class PostHandler implements HttpTestHandler { | |
| @Override | |
| public void handle(HttpTestExchange exchange) throws IOException { | |
| // Http1 server has already sent 100 response at this point but not Http2 server | |
| if (exchange.getExchangeVersion().equals(HttpClient.Version.HTTP_2)) { | |
| // Send 100 Headers, tell client that we're ready for body | |
| err.println("Server sending 100 (length = 0)"); | |
| exchange.sendResponseHeaders(100, 0); | |
| } | |
| // Read body from client and acknowledge with 200 | |
| try (InputStream is = exchange.getRequestBody()) { | |
| err.println("Server reading body"); | |
| is.readAllBytes(); | |
| err.println("Server send 200 (length=0)"); | |
| exchange.sendResponseHeaders(200, 0); | |
| } | |
| } | |
| } | |
| static class ForcePostHandler implements HttpTestHandler { | |
| @Override | |
| public void handle(HttpTestExchange exchange) throws IOException { | |
| try (InputStream is = exchange.getRequestBody()) { | |
| err.println("Server reading body inside the force Post"); | |
| is.readAllBytes(); | |
| err.println("Server send 200 (length=0) in the force post"); | |
| exchange.sendResponseHeaders(200, 0); | |
| } | |
| } | |
| } | |
| static class PostHandlerHttp2 implements Http2Handler { | |
| @Override | |
| public void handle(Http2TestExchange exchange) throws IOException { | |
| if (exchange instanceof ExpectContinueTestExchangeImpl impl) { | |
| impl.sendEndStreamHeaders(); | |
| } | |
| } | |
| } | |
| static class PostHandlerCantContinue implements HttpTestHandler { | |
| @Override | |
| public void handle(HttpTestExchange exchange) throws IOException { | |
| //Send 417 Headers, tell client to not send body | |
| try (OutputStream os = exchange.getResponseBody()) { | |
| byte[] bytes = EXPECTATION_FAILED_417.getBytes(); | |
| err.println("Server send 417 (length="+bytes.length+")"); | |
| exchange.sendResponseHeaders(417, bytes.length); | |
| err.println("Server sending Response Body"); | |
| os.write(bytes); | |
| } | |
| } | |
| } | |
| static class ExpectContinueTestExchangeImpl extends Http2TestExchangeImpl { | |
| public ExpectContinueTestExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { | |
| super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); | |
| } | |
| private void sendEndStreamHeaders() throws IOException { | |
| this.responseLength = 0; | |
| rspheadersBuilder.setHeader(":status", Integer.toString(100)); | |
| HttpHeaders headers = rspheadersBuilder.build(); | |
| Http2TestServerConnection.ResponseHeaders response | |
| = new Http2TestServerConnection.ResponseHeaders(headers); | |
| response.streamid(streamid); | |
| response.setFlag(HeaderFrame.END_HEADERS); | |
| response.setFlag(HeaderFrame.END_STREAM); | |
| sendResponseHeaders(response); | |
| } | |
| } | |
| static class Http1HangServer extends Thread implements Closeable { | |
| final ServerSocket ss; | |
| final InetAddress ia; | |
| final int port; | |
| volatile boolean closed = false; | |
| volatile Socket client; | |
| Http1HangServer(InetSocketAddress sa) throws IOException { | |
| ss = ServerSocketFactory.getDefault() | |
| .createServerSocket(sa.getPort(), -1, sa.getAddress()); | |
| ia = ss.getInetAddress(); | |
| port = ss.getLocalPort(); | |
| } | |
| @Override | |
| public void run() { | |
| byte[] bytes = EXPECTATION_FAILED_417.getBytes(); | |
| boolean closed = this.closed; | |
| while (!closed) { | |
| try { | |
| // Not using try with resources here as we expect the client to close resources when | |
| // 417 is received | |
| System.err.println("Http1HangServer accepting connections"); | |
| var client = this.client = ss.accept(); | |
| System.err.println("Http1HangServer accepted connection: " + client); | |
| InputStream is = client.getInputStream(); | |
| OutputStream os = client.getOutputStream(); | |
| BufferedReader reader = new BufferedReader(new InputStreamReader(is)); | |
| Writer w = new OutputStreamWriter(os, UTF_8); | |
| PrintWriter pw = new PrintWriter(w); | |
| StringBuilder response = new StringBuilder(); | |
| String line = null; | |
| StringBuilder reqBuilder = new StringBuilder(); | |
| while (!(line = reader.readLine()).isEmpty()) { | |
| reqBuilder.append(line + "\r\n"); | |
| } | |
| String req = reqBuilder.toString(); | |
| System.err.println("Http1HangServer received: " + req); | |
| StringTokenizer tokenizer = new StringTokenizer(req); | |
| String method = tokenizer.nextToken(); | |
| String path = tokenizer.nextToken(); | |
| String version = tokenizer.nextToken(); | |
| boolean validRequest = method.equals("POST") && path.equals("/http1/hang") | |
| && version.equals("HTTP/1.1"); | |
| // If correct request, send 417 reply. Otherwise, wait for correct one | |
| if (validRequest) { | |
| System.err.println("Http1HangServer sending 417"); | |
| closed = this.closed = true; | |
| response.append("HTTP/1.1 417 Expectation Failed\r\n") | |
| .append("Content-Length: ") | |
| .append(0) | |
| .append("\r\n\r\n"); | |
| pw.print(response); | |
| pw.flush(); | |
| os.write(bytes); | |
| os.flush(); | |
| } else { | |
| System.err.println("Http1HangServer received invalid request: closing"); | |
| client.close(); | |
| } | |
| } catch (IOException e) { | |
| closed = this.closed = true; | |
| e.printStackTrace(); | |
| } finally { | |
| if (closed = this.closed) { | |
| System.err.println("Http1HangServer: finished"); | |
| } else { | |
| System.err.println("Http1HangServer: looping for accepting next connection"); | |
| } | |
| } | |
| } | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| var client = this.client; | |
| if (client != null) client.close(); | |
| if (ss != null) ss.close(); | |
| } | |
| } | |
| private void verifyRequest(String path, int expectedStatusCode, HttpResponse<String> resp, boolean exceptionally, Throwable testThrowable) { | |
| if (!exceptionally) { | |
| err.printf("Response code %s received for path %s %n", resp.statusCode(), path); | |
| } | |
| if (exceptionally && testThrowable != null) { | |
| err.println("Finished exceptionally Test throwable: " + testThrowable); | |
| assertEquals(testThrowable.getClass(), ProtocolException.class); | |
| } else if (exceptionally) { | |
| throw new TestException("Expected case to finish with an IOException but testException is null"); | |
| } else if (resp != null) { | |
| assertEquals(resp.statusCode(), expectedStatusCode); | |
| err.println("Request completed successfully for path " + path); | |
| err.println("Response Headers: " + resp.headers()); | |
| err.println("Response Status Code: " + resp.statusCode()); | |
| } | |
| } | |
| } |