03bc6b359f
Reviewed-by: aefimov, michaelm
376 lines
17 KiB
Java
376 lines
17 KiB
Java
/*
|
|
* 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());
|
|
}
|
|
}
|
|
}
|