8292044: HttpClient doesn't handle 102 or 103 properly
Reviewed-by: dfuchs, chegar, michaelm
This commit is contained in:
parent
83abfa5d82
commit
800e68d690
src/java.net.http/share/classes/jdk/internal/net/http
test/jdk/java/net/httpclient
@ -27,6 +27,7 @@ package jdk.internal.net.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -472,10 +473,62 @@ final class Exchange<T> {
|
||||
CompletableFuture<Response> cf = ex.sendBodyAsync()
|
||||
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
|
||||
cf = wrapForUpgrade(cf);
|
||||
// after 101 is handled we check for other 1xx responses
|
||||
cf = cf.thenCompose(this::ignore1xxResponse);
|
||||
cf = wrapForLog(cf);
|
||||
return cf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the passed Response has a status code between 102 and 199 (both inclusive).
|
||||
* If so, then that {@code Response} is considered intermediate informational response and is
|
||||
* ignored by the client. This method then creates a new {@link CompletableFuture} which
|
||||
* completes when a subsequent response is sent by the server. Such newly constructed
|
||||
* {@link CompletableFuture} will not complete till a "final" response (one which doesn't have
|
||||
* a response code between 102 and 199 inclusive) is sent by the server. The returned
|
||||
* {@link CompletableFuture} is thus capable of handling multiple subsequent intermediate
|
||||
* informational responses from the server.
|
||||
* <p>
|
||||
* If the passed Response doesn't have a status code between 102 and 199 (both inclusive) then
|
||||
* this method immediately returns back a completed {@link CompletableFuture} with the passed
|
||||
* {@code Response}.
|
||||
* </p>
|
||||
*
|
||||
* @param rsp The response
|
||||
* @return A {@code CompletableFuture} with the final response from the server
|
||||
*/
|
||||
private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
|
||||
final int statusCode = rsp.statusCode();
|
||||
// we ignore any response code which is 1xx.
|
||||
// For 100 (with the request configured to expect-continue) and 101, we handle it
|
||||
// specifically as defined in the RFC-9110, outside of this method.
|
||||
// As noted in RFC-9110, section 15.2.1, if response code is 100 and if the request wasn't
|
||||
// configured with expectContinue, then we ignore the 100 response and wait for the final
|
||||
// response (just like any other 1xx response).
|
||||
// Any other response code between 102 and 199 (both inclusive) aren't specified in the
|
||||
// "HTTP semantics" RFC-9110. The spec states that these 1xx response codes are informational
|
||||
// and interim and the client can choose to ignore them and continue to wait for the
|
||||
// final response (headers)
|
||||
if ((statusCode >= 102 && statusCode <= 199)
|
||||
|| (statusCode == 100 && !request.expectContinue)) {
|
||||
Log.logTrace("Ignoring (1xx informational) response code {0}", rsp.statusCode());
|
||||
if (debug.on()) {
|
||||
debug.log("Ignoring (1xx informational) response code "
|
||||
+ rsp.statusCode());
|
||||
}
|
||||
assert exchImpl != null : "Illegal state - current exchange isn't set";
|
||||
// ignore this Response and wait again for the subsequent response headers
|
||||
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
|
||||
// we recompose the CF again into the ignore1xxResponse check/function because
|
||||
// the 1xx response is allowed to be sent multiple times for a request, before
|
||||
// a final response arrives
|
||||
return cf.thenCompose(this::ignore1xxResponse);
|
||||
} else {
|
||||
// return the already completed future
|
||||
return MinimalFuture.completedFuture(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
|
||||
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
|
||||
bodyIgnored = null;
|
||||
@ -506,7 +559,30 @@ final class Exchange<T> {
|
||||
if (upgrading) {
|
||||
return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
|
||||
}
|
||||
return cf;
|
||||
// websocket requests use "Connection: Upgrade" and "Upgrade: websocket" headers.
|
||||
// however, the "upgrading" flag we maintain in this class only tracks a h2 upgrade
|
||||
// that we internally triggered. So it will be false in the case of websocket upgrade, hence
|
||||
// this additional check. If it's a websocket request we allow 101 responses and we don't
|
||||
// require any additional checks when a response arrives.
|
||||
if (request.isWebSocket()) {
|
||||
return cf;
|
||||
}
|
||||
// not expecting an upgrade, but if the server sends a 101 response then we fail the
|
||||
// request and also let the ExchangeImpl deal with it as a protocol error
|
||||
return cf.thenCompose(r -> {
|
||||
if (r.statusCode == 101) {
|
||||
final ProtocolException protoEx = new ProtocolException("Unexpected 101 " +
|
||||
"response, when not upgrading");
|
||||
assert exchImpl != null : "Illegal state - current exchange isn't set";
|
||||
try {
|
||||
exchImpl.onProtocolError(protoEx);
|
||||
} catch (Throwable ignore){
|
||||
// ignored
|
||||
}
|
||||
return MinimalFuture.failedFuture(protoEx);
|
||||
}
|
||||
return MinimalFuture.completedFuture(r);
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {
|
||||
|
@ -216,6 +216,16 @@ abstract class ExchangeImpl<T> {
|
||||
*/
|
||||
abstract void cancel(IOException cause);
|
||||
|
||||
/**
|
||||
* Invoked whenever there is a (HTTP) protocol error when dealing with the response
|
||||
* from the server. The implementations of {@code ExchangeImpl} are then expected to
|
||||
* take necessary action that is expected by the corresponding specifications whenever
|
||||
* a protocol error happens. For example, in HTTP/1.1, such protocol error would result
|
||||
* in the connection being closed.
|
||||
* @param cause The cause of the protocol violation
|
||||
*/
|
||||
abstract void onProtocolError(IOException cause);
|
||||
|
||||
/**
|
||||
* Called when the exchange is released, so that cleanup actions may be
|
||||
* performed - such as deregistering callbacks.
|
||||
|
@ -502,6 +502,15 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
||||
cancelImpl(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onProtocolError(final IOException cause) {
|
||||
if (debug.on()) {
|
||||
debug.log("cancelling exchange due to protocol error: %s", cause.getMessage());
|
||||
}
|
||||
Log.logError("cancelling exchange due to protocol error: {0}\n", cause);
|
||||
cancelImpl(cause);
|
||||
}
|
||||
|
||||
private void cancelImpl(Throwable cause) {
|
||||
LinkedList<CompletableFuture<?>> toComplete = null;
|
||||
int count = 0;
|
||||
|
@ -346,7 +346,7 @@ class Http2Connection {
|
||||
sendConnectionPreface();
|
||||
if (!opened) {
|
||||
debug.log("ensure reset frame is sent to cancel initial stream");
|
||||
initialStream.sendCancelStreamFrame();
|
||||
initialStream.sendResetStreamFrame(ResetFrame.CANCEL);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import java.net.Authenticator;
|
||||
import java.net.ConnectException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpConnectTimeoutException;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
@ -859,6 +860,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
||||
// any other SSLException is wrapped in a plain
|
||||
// SSLException
|
||||
throw new SSLException(msg, throwable);
|
||||
} else if (throwable instanceof ProtocolException) {
|
||||
throw new ProtocolException(msg);
|
||||
} else if (throwable instanceof IOException) {
|
||||
throw new IOException(msg, throwable);
|
||||
} else {
|
||||
|
@ -449,7 +449,7 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
private boolean checkRequestCancelled() {
|
||||
if (exchange.multi.requestCancelled()) {
|
||||
if (errorRef.get() == null) cancel();
|
||||
else sendCancelStreamFrame();
|
||||
else sendResetStreamFrame(ResetFrame.CANCEL);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -1238,6 +1238,16 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
cancelImpl(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onProtocolError(final IOException cause) {
|
||||
if (debug.on()) {
|
||||
debug.log("cancelling exchange on stream %d due to protocol error: %s", streamid, cause.getMessage());
|
||||
}
|
||||
Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamid, cause);
|
||||
// send a RESET frame and close the stream
|
||||
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
|
||||
}
|
||||
|
||||
void connectionClosing(Throwable cause) {
|
||||
Flow.Subscriber<?> subscriber =
|
||||
responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
|
||||
@ -1249,6 +1259,10 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
|
||||
// This method sends a RST_STREAM frame
|
||||
void cancelImpl(Throwable e) {
|
||||
cancelImpl(e, ResetFrame.CANCEL);
|
||||
}
|
||||
|
||||
private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
|
||||
errorRef.compareAndSet(null, e);
|
||||
if (debug.on()) {
|
||||
if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
|
||||
@ -1280,14 +1294,14 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
try {
|
||||
// will send a RST_STREAM frame
|
||||
if (streamid != 0 && streamState == 0) {
|
||||
e = Utils.getCompletionCause(e);
|
||||
if (e instanceof EOFException) {
|
||||
final Throwable cause = Utils.getCompletionCause(e);
|
||||
if (cause instanceof EOFException) {
|
||||
// read EOF: no need to try & send reset
|
||||
connection.decrementStreamsCount(streamid);
|
||||
connection.closeStream(streamid);
|
||||
} else {
|
||||
// no use to send CANCEL if already closed.
|
||||
sendCancelStreamFrame();
|
||||
sendResetStreamFrame(resetFrameErrCode);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
@ -1295,10 +1309,10 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
}
|
||||
}
|
||||
|
||||
void sendCancelStreamFrame() {
|
||||
void sendResetStreamFrame(final int resetFrameErrCode) {
|
||||
// do not reset a stream until it has a streamid.
|
||||
if (streamid > 0 && markStream(ResetFrame.CANCEL) == 0) {
|
||||
connection.resetStream(streamid, ResetFrame.CANCEL);
|
||||
if (streamid > 0 && markStream(resetFrameErrCode) == 0) {
|
||||
connection.resetStream(streamid, resetFrameErrCode);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
495
test/jdk/java/net/httpclient/Response1xxTest.java
Normal file
495
test/jdk/java/net/httpclient/Response1xxTest.java
Normal file
@ -0,0 +1,495 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.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;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @bug 8292044
|
||||
* @summary Tests behaviour of HttpClient when server responds with 102 or 103 status codes
|
||||
* @modules java.base/sun.net.www.http
|
||||
* java.net.http/jdk.internal.net.http.common
|
||||
* java.net.http/jdk.internal.net.http.frame
|
||||
* java.net.http/jdk.internal.net.http.hpack
|
||||
* java.logging
|
||||
* jdk.httpserver
|
||||
* @library /test/lib http2/server
|
||||
* @build Http2TestServer HttpServerAdapters jdk.test.lib.net.SimpleSSLContext
|
||||
* @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.of(new Http2TestServer("localhost", false, 0));
|
||||
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.of(new Http2TestServer("localhost",
|
||||
true, 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(HttpClient.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(), HttpClient.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(HttpClient.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(HttpClient.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(HttpClient.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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user