mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2025-12-06 01:19:28 +01:00
8208693: HttpClient: Extend the request timeout's scope to cover the response body
Reviewed-by: jpai, dfuchs
This commit is contained in:
@@ -312,10 +312,22 @@ public abstract class HttpClient implements AutoCloseable {
|
||||
* need to be established, for example if a connection can be reused
|
||||
* from a previous request, then this timeout duration has no effect.
|
||||
*
|
||||
* @implSpec
|
||||
* A connection timeout applies to the entire connection phase, from the
|
||||
* moment a connection is requested until it is established.
|
||||
* Implementations are recommended to ensure that the connection timeout
|
||||
* covers any SSL/TLS handshakes.
|
||||
*
|
||||
* @implNote
|
||||
* The built-in JDK implementation of the connection timeout covers any
|
||||
* SSL/TLS handshakes.
|
||||
*
|
||||
* @param duration the duration to allow the underlying connection to be
|
||||
* established
|
||||
* @return this builder
|
||||
* @throws IllegalArgumentException if the duration is non-positive
|
||||
* @see HttpRequest.Builder#timeout(Duration) Configuring timeout for
|
||||
* request execution
|
||||
*/
|
||||
public Builder connectTimeout(Duration duration);
|
||||
|
||||
|
||||
@@ -258,12 +258,28 @@ public abstract class HttpRequest {
|
||||
* {@link HttpClient#sendAsync(java.net.http.HttpRequest,
|
||||
* java.net.http.HttpResponse.BodyHandler) HttpClient::sendAsync}
|
||||
* completes exceptionally with an {@code HttpTimeoutException}. The effect
|
||||
* of not setting a timeout is the same as setting an infinite Duration,
|
||||
* i.e. block forever.
|
||||
* of not setting a timeout is the same as setting an infinite
|
||||
* {@code Duration}, i.e., block forever.
|
||||
*
|
||||
* @implSpec
|
||||
* A timeout applies to the duration measured from the instant the
|
||||
* request execution starts to, <em>at least</em>, the instant an
|
||||
* {@link HttpResponse} is constructed. The elapsed time includes
|
||||
* obtaining a connection for transport and retrieving the response
|
||||
* headers.
|
||||
*
|
||||
* @implNote
|
||||
* The JDK built-in implementation applies timeout over the duration
|
||||
* measured from the instant the request execution starts to <b>the
|
||||
* instant the response body is consumed</b>, if present. This is
|
||||
* implemented by stopping the timer after the response body subscriber
|
||||
* completion.
|
||||
*
|
||||
* @param duration the timeout duration
|
||||
* @return this builder
|
||||
* @throws IllegalArgumentException if the duration is non-positive
|
||||
* @see HttpClient.Builder#connectTimeout(Duration) Configuring
|
||||
* timeout for connection establishment
|
||||
*/
|
||||
public abstract Builder timeout(Duration duration);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2015, 2025, 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
|
||||
@@ -144,6 +144,16 @@ public interface WebSocket {
|
||||
* {@link HttpTimeoutException}. If this method is not invoked then the
|
||||
* infinite timeout is assumed.
|
||||
*
|
||||
* @implSpec
|
||||
* A connection timeout applies to the entire connection phase, from the
|
||||
* moment a connection is requested until it is established.
|
||||
* Implementations are recommended to ensure that the connection timeout
|
||||
* covers any WebSocket and SSL/TLS handshakes.
|
||||
*
|
||||
* @implNote
|
||||
* The built-in JDK implementation of the connection timeout covers any
|
||||
* WebSocket and SSL/TLS handshakes.
|
||||
*
|
||||
* @param timeout
|
||||
* the timeout, non-{@linkplain Duration#isNegative() negative},
|
||||
* non-{@linkplain Duration#ZERO ZERO}
|
||||
|
||||
@@ -581,6 +581,18 @@ abstract class ExchangeImpl<T> {
|
||||
// Needed for HTTP/2 to subscribe a dummy subscriber and close the stream
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return {@code true}, if it is allowed to cancel the request timer on
|
||||
* response body subscriber termination; {@code false}, otherwise}
|
||||
*
|
||||
* @param webSocket indicates if the associated request is a WebSocket handshake
|
||||
* @param statusCode the status code of the associated response
|
||||
*/
|
||||
static boolean cancelTimerOnResponseBodySubscriberTermination(
|
||||
boolean webSocket, int statusCode) {
|
||||
return webSocket || statusCode < 100 || statusCode >= 200;
|
||||
}
|
||||
|
||||
/* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
|
||||
|
||||
abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();
|
||||
|
||||
@@ -206,8 +206,15 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
||||
*/
|
||||
static final class Http1ResponseBodySubscriber<U> extends HttpBodySubscriberWrapper<U> {
|
||||
final Http1Exchange<U> exchange;
|
||||
Http1ResponseBodySubscriber(BodySubscriber<U> userSubscriber, Http1Exchange<U> exchange) {
|
||||
|
||||
private final boolean cancelTimerOnTermination;
|
||||
|
||||
Http1ResponseBodySubscriber(
|
||||
BodySubscriber<U> userSubscriber,
|
||||
boolean cancelTimerOnTermination,
|
||||
Http1Exchange<U> exchange) {
|
||||
super(userSubscriber);
|
||||
this.cancelTimerOnTermination = cancelTimerOnTermination;
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
@@ -220,6 +227,14 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
||||
protected void unregister() {
|
||||
exchange.unregisterResponseSubscriber(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTermination() {
|
||||
if (cancelTimerOnTermination) {
|
||||
exchange.exchange.multi.cancelTimer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -459,9 +474,10 @@ class Http1Exchange<T> extends ExchangeImpl<T> {
|
||||
@Override
|
||||
Http1ResponseBodySubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
|
||||
BodySubscriber<T> subscriber = handler.apply(response);
|
||||
Http1ResponseBodySubscriber<T> bs =
|
||||
new Http1ResponseBodySubscriber<T>(subscriber, this);
|
||||
return bs;
|
||||
var cancelTimerOnTermination =
|
||||
cancelTimerOnResponseBodySubscriberTermination(
|
||||
exchange.request().isWebSocket(), response.statusCode());
|
||||
return new Http1ResponseBodySubscriber<>(subscriber, cancelTimerOnTermination, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -554,8 +554,12 @@ final class Http3ExchangeImpl<T> extends Http3Stream<T> {
|
||||
}
|
||||
|
||||
final class Http3StreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {
|
||||
Http3StreamResponseSubscriber(BodySubscriber<U> subscriber) {
|
||||
|
||||
private final boolean cancelTimerOnTermination;
|
||||
|
||||
Http3StreamResponseSubscriber(BodySubscriber<U> subscriber, boolean cancelTimerOnTermination) {
|
||||
super(subscriber);
|
||||
this.cancelTimerOnTermination = cancelTimerOnTermination;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -568,6 +572,13 @@ final class Http3ExchangeImpl<T> extends Http3Stream<T> {
|
||||
registerResponseSubscriber(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTermination() {
|
||||
if (cancelTimerOnTermination) {
|
||||
exchange.multi.cancelTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logComplete(Throwable error) {
|
||||
if (error == null) {
|
||||
@@ -590,9 +601,10 @@ final class Http3ExchangeImpl<T> extends Http3Stream<T> {
|
||||
Http3StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler,
|
||||
ResponseInfo response) {
|
||||
if (debug.on()) debug.log("Creating body subscriber");
|
||||
Http3StreamResponseSubscriber<T> subscriber =
|
||||
new Http3StreamResponseSubscriber<>(handler.apply(response));
|
||||
return subscriber;
|
||||
var cancelTimerOnTermination =
|
||||
cancelTimerOnResponseBodySubscriberTermination(
|
||||
exchange.request().isWebSocket(), response.statusCode());
|
||||
return new Http3StreamResponseSubscriber<>(handler.apply(response), cancelTimerOnTermination);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1880,6 +1880,13 @@ final class HttpClientImpl extends HttpClient implements Trackable {
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for tests
|
||||
List<TimeoutEvent> timers() {
|
||||
synchronized (this) {
|
||||
return new ArrayList<>(timeouts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges ( handles ) timer events that have passed their deadline, and
|
||||
* returns the amount of time, in milliseconds, until the next earliest
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
package jdk.internal.net.http;
|
||||
|
||||
import java.io.IOError;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.ConnectException;
|
||||
@@ -254,7 +253,7 @@ class MultiExchange<T> implements Cancelable {
|
||||
.map(ConnectTimeoutTracker::getRemaining);
|
||||
}
|
||||
|
||||
private void cancelTimer() {
|
||||
void cancelTimer() {
|
||||
if (responseTimerEvent != null) {
|
||||
client.cancelTimer(responseTimerEvent);
|
||||
responseTimerEvent = null;
|
||||
@@ -404,6 +403,8 @@ class MultiExchange<T> implements Cancelable {
|
||||
processAltSvcHeader(r, client(), currentreq);
|
||||
Exchange<T> exch = getExchange();
|
||||
if (bodyNotPermitted(r)) {
|
||||
// No response body consumption is expected, we can cancel the timer right away
|
||||
cancelTimer();
|
||||
if (bodyIsPresent(r)) {
|
||||
IOException ioe = new IOException(
|
||||
"unexpected content length header with 204 response");
|
||||
@@ -467,6 +468,8 @@ class MultiExchange<T> implements Cancelable {
|
||||
|
||||
private CompletableFuture<Response> responseAsyncImpl(final boolean applyReqFilters) {
|
||||
if (currentreq.timeout().isPresent()) {
|
||||
// Retried/Forwarded requests should reset the timer, if present
|
||||
cancelTimer();
|
||||
responseTimerEvent = ResponseTimerEvent.of(this);
|
||||
client.registerTimer(responseTimerEvent);
|
||||
}
|
||||
@@ -502,7 +505,6 @@ class MultiExchange<T> implements Cancelable {
|
||||
}
|
||||
return completedFuture(response);
|
||||
} else {
|
||||
cancelTimer();
|
||||
setNewResponse(currentreq, response, null, exch);
|
||||
if (currentreq.isWebSocket()) {
|
||||
// need to close the connection and open a new one.
|
||||
@@ -520,11 +522,18 @@ class MultiExchange<T> implements Cancelable {
|
||||
} })
|
||||
.handle((response, ex) -> {
|
||||
// 5. handle errors and cancel any timer set
|
||||
cancelTimer();
|
||||
if (ex == null) {
|
||||
assert response != null;
|
||||
return completedFuture(response);
|
||||
}
|
||||
|
||||
// Cancel the timer. Note that we only do so if the
|
||||
// response has completed exceptionally. That is, we don't
|
||||
// cancel the timer if there are no exceptions, since the
|
||||
// response body might still get consumed, and it is
|
||||
// still subject to the response timer.
|
||||
cancelTimer();
|
||||
|
||||
// all exceptions thrown are handled here
|
||||
final RetryContext retryCtx = checkRetryEligible(ex, exch);
|
||||
assert retryCtx != null : "retry context is null";
|
||||
|
||||
@@ -390,9 +390,10 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
|
||||
@Override
|
||||
Http2StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
|
||||
Http2StreamResponseSubscriber<T> subscriber =
|
||||
new Http2StreamResponseSubscriber<>(handler.apply(response));
|
||||
return subscriber;
|
||||
var cancelTimerOnTermination =
|
||||
cancelTimerOnResponseBodySubscriberTermination(
|
||||
exchange.request().isWebSocket(), response.statusCode());
|
||||
return new Http2StreamResponseSubscriber<>(handler.apply(response), cancelTimerOnTermination);
|
||||
}
|
||||
|
||||
// The Http2StreamResponseSubscriber is registered with the HttpClient
|
||||
@@ -1694,6 +1695,11 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
.whenComplete((v, t) -> pushGroup.pushError(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
Http2StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
|
||||
return new Http2StreamResponseSubscriber<T>(handler.apply(response), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
void completeResponse(Response r) {
|
||||
Log.logResponse(r::toString);
|
||||
@@ -1924,8 +1930,12 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
}
|
||||
|
||||
final class Http2StreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {
|
||||
Http2StreamResponseSubscriber(BodySubscriber<U> subscriber) {
|
||||
|
||||
private final boolean cancelTimerOnTermination;
|
||||
|
||||
Http2StreamResponseSubscriber(BodySubscriber<U> subscriber, boolean cancelTimerOnTermination) {
|
||||
super(subscriber);
|
||||
this.cancelTimerOnTermination = cancelTimerOnTermination;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1938,6 +1948,13 @@ class Stream<T> extends ExchangeImpl<T> {
|
||||
unregisterResponseSubscriber(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTermination() {
|
||||
if (cancelTimerOnTermination) {
|
||||
exchange.multi.cancelTimer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final VarHandle DEREGISTERED;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2022, 2025, 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
|
||||
@@ -33,7 +33,6 @@ import java.util.Objects;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.Flow.Subscription;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -51,7 +50,6 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
|
||||
public static final Comparator<HttpBodySubscriberWrapper<?>> COMPARE_BY_ID
|
||||
= Comparator.comparing(HttpBodySubscriberWrapper::id);
|
||||
|
||||
|
||||
public static final Flow.Subscription NOP = new Flow.Subscription() {
|
||||
@Override
|
||||
public void request(long n) { }
|
||||
@@ -75,7 +73,18 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
|
||||
this.userSubscriber = userSubscriber;
|
||||
}
|
||||
|
||||
private class SubscriptionWrapper implements Subscription {
|
||||
/**
|
||||
* A callback to be invoked <em>before</em> termination, whether due to the
|
||||
* completion or failure of the subscriber, or cancellation of the
|
||||
* subscription. To be precise, this method is called before
|
||||
* {@link #onComplete()}, {@link #onError(Throwable) onError()}, or
|
||||
* {@link #onCancel()}.
|
||||
*/
|
||||
protected void onTermination() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private final class SubscriptionWrapper implements Subscription {
|
||||
final Subscription subscription;
|
||||
SubscriptionWrapper(Subscription s) {
|
||||
this.subscription = Objects.requireNonNull(s);
|
||||
@@ -92,6 +101,7 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
|
||||
subscription.cancel();
|
||||
} finally {
|
||||
if (markCancelled()) {
|
||||
onTermination();
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
@@ -284,6 +294,7 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
|
||||
*/
|
||||
public final void complete(Throwable t) {
|
||||
if (markCompleted()) {
|
||||
onTermination();
|
||||
logComplete(t);
|
||||
tryUnregister();
|
||||
t = withError = Utils.getCompletionCause(t);
|
||||
|
||||
285
test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java
Normal file
285
test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright (c) 2025, 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 jdk.internal.net.http.common.Logger;
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
import static jdk.internal.net.http.HttpClientTimerAccess.assertNoResponseTimerEventRegistrations;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/*
|
||||
* @test id=retriesDisabled
|
||||
* @bug 8208693
|
||||
* @summary Verifies `HttpRequest::timeout` is effective for *response body*
|
||||
* timeouts when all retry mechanisms are disabled.
|
||||
*
|
||||
* @library /test/lib
|
||||
* /test/jdk/java/net/httpclient/lib
|
||||
* access
|
||||
* @build TimeoutResponseTestSupport
|
||||
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
*
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.auth.retrylimit=0
|
||||
* -Djdk.httpclient.disableRetryConnect
|
||||
* -Djdk.httpclient.redirects.retrylimit=0
|
||||
* -Dtest.requestTimeoutMillis=1000
|
||||
* TimeoutResponseBodyTest
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test id=retriesEnabledForResponseFailure
|
||||
* @bug 8208693
|
||||
* @summary Verifies `HttpRequest::timeout` is effective for *response body*
|
||||
* timeouts, where some initial responses are intentionally configured
|
||||
* to fail to trigger retries.
|
||||
*
|
||||
* @library /test/lib
|
||||
* /test/jdk/java/net/httpclient/lib
|
||||
* access
|
||||
* @build TimeoutResponseTestSupport
|
||||
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
*
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.auth.retrylimit=0
|
||||
* -Djdk.httpclient.disableRetryConnect
|
||||
* -Djdk.httpclient.redirects.retrylimit=3
|
||||
* -Dtest.requestTimeoutMillis=1000
|
||||
* -Dtest.responseFailureWaitDurationMillis=600
|
||||
* TimeoutResponseBodyTest
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verifies {@link HttpRequest#timeout() HttpRequest.timeout()} is effective
|
||||
* for <b>response body</b> timeouts.
|
||||
*
|
||||
* @implNote
|
||||
*
|
||||
* Using a response body subscriber (i.e., {@link InputStream}) of type that
|
||||
* allows gradual consumption of the response body after successfully building
|
||||
* an {@link HttpResponse} instance to ensure timeouts are propagated even
|
||||
* after the {@code HttpResponse} construction.
|
||||
* <p>
|
||||
* Each test is provided a pristine ephemeral client to avoid any unexpected
|
||||
* effects due to pooling.
|
||||
*/
|
||||
class TimeoutResponseBodyTest extends TimeoutResponseTestSupport {
|
||||
|
||||
private static final Logger LOGGER = Utils.getDebugLogger(
|
||||
TimeoutResponseBodyTest.class.getSimpleName()::toString, Utils.DEBUG);
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) HttpClient::send}
|
||||
* against a server blocking without delivering the response body.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendOnMissingBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.BLOCK_BEFORE_BODY_DELIVERY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request");
|
||||
var response = client.send(pair.request(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
LOGGER.log("Consuming the obtained response");
|
||||
verifyResponseBodyDoesNotArrive(response);
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler) HttpClient::sendAsync}
|
||||
* against a server blocking without delivering the response body.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendAsyncOnMissingBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.BLOCK_BEFORE_BODY_DELIVERY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request asynchronously");
|
||||
var responseFuture = client.sendAsync(pair.request(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
LOGGER.log("Obtaining the response");
|
||||
var response = responseFuture.get();
|
||||
LOGGER.log("Consuming the obtained response");
|
||||
verifyResponseBodyDoesNotArrive(response);
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void verifyResponseBodyDoesNotArrive(HttpResponse<InputStream> response) {
|
||||
assertEquals(200, response.statusCode());
|
||||
assertThrowsHttpTimeoutException(() -> {
|
||||
try (var responseBodyStream = response.body()) {
|
||||
var readByte = responseBodyStream.read();
|
||||
fail("Unexpected read byte: " + readByte);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) HttpClient::send}
|
||||
* against a server delivering the response body very slowly.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendOnSlowBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.DELIVER_BODY_SLOWLY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request");
|
||||
var response = client.send(pair.request(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
LOGGER.log("Consuming the obtained response");
|
||||
verifyResponseBodyArrivesSlow(response);
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler) HttpClient::sendAsync}
|
||||
* against a server delivering the response body very slowly.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendAsyncOnSlowBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.DELIVER_BODY_SLOWLY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request asynchronously");
|
||||
var responseFuture = client.sendAsync(pair.request(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
LOGGER.log("Obtaining the response");
|
||||
var response = responseFuture.get();
|
||||
LOGGER.log("Consuming the obtained response");
|
||||
verifyResponseBodyArrivesSlow(response);
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void verifyResponseBodyArrivesSlow(HttpResponse<InputStream> response) {
|
||||
assertEquals(200, response.statusCode());
|
||||
assertThrowsHttpTimeoutException(() -> {
|
||||
try (var responseBodyStream = response.body()) {
|
||||
int i = 0;
|
||||
int l = ServerRequestPair.CONTENT_LENGTH;
|
||||
for (; i < l; i++) {
|
||||
LOGGER.log("Reading byte %s/%s", i, l);
|
||||
var readByte = responseBodyStream.read();
|
||||
if (readByte < 0) {
|
||||
break;
|
||||
}
|
||||
assertEquals(i, readByte);
|
||||
}
|
||||
fail("Should not have reached here! (i=%s)".formatted(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) HttpClient::send}
|
||||
* against a server delivering 204, i.e., no content, which is handled
|
||||
* through a specialized path served by {@code MultiExchange::handleNoBody}.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendOnNoBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.DELIVER_NO_BODY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request");
|
||||
client.send(pair.request(), HttpResponse.BodyHandlers.discarding());
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler) HttpClient::sendAsync}
|
||||
* against a server delivering 204, i.e., no content, which is handled
|
||||
* through a specialized path served by {@code MultiExchange::handleNoBody}.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendAsyncOnNoBody(ServerRequestPair pair) throws Exception {
|
||||
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.DELIVER_NO_BODY;
|
||||
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request asynchronously");
|
||||
client.sendAsync(pair.request(), HttpResponse.BodyHandlers.discarding()).get();
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
138
test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java
Normal file
138
test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2025, 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 jdk.internal.net.http.common.Logger;
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
import static jdk.internal.net.http.HttpClientTimerAccess.assertNoResponseTimerEventRegistrations;
|
||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
|
||||
/*
|
||||
* @test id=retriesDisabled
|
||||
* @bug 8208693
|
||||
* @summary Verifies `HttpRequest::timeout` is effective for *response header*
|
||||
* timeouts when all retry mechanisms are disabled.
|
||||
*
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* access
|
||||
* @build TimeoutResponseTestSupport
|
||||
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
*
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.auth.retrylimit=0
|
||||
* -Djdk.httpclient.disableRetryConnect
|
||||
* -Djdk.httpclient.redirects.retrylimit=0
|
||||
* -Dtest.requestTimeoutMillis=1000
|
||||
* TimeoutResponseHeaderTest
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test id=retriesEnabledForResponseFailure
|
||||
* @bug 8208693
|
||||
* @summary Verifies `HttpRequest::timeout` is effective for *response header*
|
||||
* timeouts, where some initial responses are intentionally configured
|
||||
* to fail to trigger retries.
|
||||
*
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* access
|
||||
* @build TimeoutResponseTestSupport
|
||||
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
|
||||
* jdk.httpclient.test.lib.common.HttpServerAdapters
|
||||
* jdk.test.lib.net.SimpleSSLContext
|
||||
*
|
||||
* @run junit/othervm
|
||||
* -Djdk.httpclient.auth.retrylimit=0
|
||||
* -Djdk.httpclient.disableRetryConnect
|
||||
* -Djdk.httpclient.redirects.retrylimit=3
|
||||
* -Dtest.requestTimeoutMillis=1000
|
||||
* -Dtest.responseFailureWaitDurationMillis=600
|
||||
* TimeoutResponseHeaderTest
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verifies {@link HttpRequest#timeout() HttpRequest.timeout()} is effective
|
||||
* for <b>response header</b> timeouts.
|
||||
*/
|
||||
class TimeoutResponseHeaderTest extends TimeoutResponseTestSupport {
|
||||
|
||||
private static final Logger LOGGER = Utils.getDebugLogger(
|
||||
TimeoutResponseHeaderTest.class.getSimpleName()::toString, Utils.DEBUG);
|
||||
|
||||
static {
|
||||
ServerRequestPair.SERVER_HANDLER_BEHAVIOUR =
|
||||
ServerRequestPair.ServerHandlerBehaviour.BLOCK_BEFORE_HEADER_DELIVERY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#send(HttpRequest, HttpResponse.BodyHandler) HttpClient::send}
|
||||
* against a server blocking without delivering any response headers.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSend(ServerRequestPair pair) throws Exception {
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(
|
||||
REQUEST_TIMEOUT.multipliedBy(2),
|
||||
() -> assertThrowsHttpTimeoutException(() -> {
|
||||
LOGGER.log("Sending the request");
|
||||
client.send(pair.request(), HttpResponse.BodyHandlers.discarding());
|
||||
}));
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests timeouts using
|
||||
* {@link HttpClient#sendAsync(HttpRequest, HttpResponse.BodyHandler) HttpClient::sendAsync}
|
||||
* against a server blocking without delivering any response headers.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("serverRequestPairs")
|
||||
void testSendAsync(ServerRequestPair pair) throws Exception {
|
||||
try (var client = pair.createClientWithEstablishedConnection()) {
|
||||
assertTimeoutPreemptively(REQUEST_TIMEOUT.multipliedBy(2), () -> {
|
||||
LOGGER.log("Sending the request asynchronously");
|
||||
var responseFuture = client.sendAsync(pair.request(), HttpResponse.BodyHandlers.discarding());
|
||||
assertThrowsHttpTimeoutException(() -> {
|
||||
LOGGER.log("Obtaining the response");
|
||||
responseFuture.get();
|
||||
});
|
||||
});
|
||||
LOGGER.log("Verifying the registered response timer events");
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
415
test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java
Normal file
415
test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java
Normal file
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* Copyright (c) 2025, 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 jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler;
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer;
|
||||
import jdk.internal.net.http.common.Logger;
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import jdk.internal.net.http.frame.ErrorFrame;
|
||||
import jdk.internal.net.http.http3.Http3Error;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpOption;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
|
||||
import static jdk.httpclient.test.lib.common.HttpServerAdapters.createClientBuilderFor;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Utilities for {@code TimeoutResponse*Test}s.
|
||||
*
|
||||
* @see TimeoutResponseBodyTest Server <b>response body</b> timeout tests
|
||||
* @see TimeoutResponseHeaderTest Server <b>response header</b> timeout tests
|
||||
* @see TimeoutBasic Server <b>connection</b> timeout tests
|
||||
*/
|
||||
public class TimeoutResponseTestSupport {
|
||||
|
||||
private static final String CLASS_NAME = TimeoutResponseTestSupport.class.getSimpleName();
|
||||
|
||||
private static final Logger LOGGER = Utils.getDebugLogger(CLASS_NAME::toString, Utils.DEBUG);
|
||||
|
||||
private static final SSLContext SSL_CONTEXT = createSslContext();
|
||||
|
||||
protected static final Duration REQUEST_TIMEOUT =
|
||||
Duration.ofMillis(Long.parseLong(System.getProperty("test.requestTimeoutMillis")));
|
||||
|
||||
static {
|
||||
assertTrue(
|
||||
REQUEST_TIMEOUT.isPositive(),
|
||||
"was expecting `test.requestTimeoutMillis > 0`, found: " + REQUEST_TIMEOUT);
|
||||
}
|
||||
|
||||
protected static final int RETRY_LIMIT =
|
||||
Integer.parseInt(System.getProperty("jdk.httpclient.redirects.retrylimit", "0"));
|
||||
|
||||
private static final long RESPONSE_FAILURE_WAIT_DURATION_MILLIS =
|
||||
Long.parseLong(System.getProperty("test.responseFailureWaitDurationMillis", "0"));
|
||||
|
||||
static {
|
||||
if (RETRY_LIMIT > 0) {
|
||||
|
||||
// Verify that response failure wait duration is provided
|
||||
if (RESPONSE_FAILURE_WAIT_DURATION_MILLIS <= 0) {
|
||||
var message = String.format(
|
||||
"`jdk.httpclient.redirects.retrylimit` (%s) is greater than zero. " +
|
||||
"`test.responseFailureWaitDurationMillis` (%s) must be greater than zero too.",
|
||||
RETRY_LIMIT, RESPONSE_FAILURE_WAIT_DURATION_MILLIS);
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
|
||||
// Verify that the total response failure waits exceed the request timeout
|
||||
var totalResponseFailureWaitDuration = Duration
|
||||
.ofMillis(RESPONSE_FAILURE_WAIT_DURATION_MILLIS)
|
||||
.multipliedBy(RETRY_LIMIT);
|
||||
if (totalResponseFailureWaitDuration.compareTo(REQUEST_TIMEOUT) <= 0) {
|
||||
var message = ("`test.responseFailureWaitDurationMillis * jdk.httpclient.redirects.retrylimit` (%s * %s = %s) " +
|
||||
"must be greater than `test.requestTimeoutMillis` (%s)")
|
||||
.formatted(
|
||||
RESPONSE_FAILURE_WAIT_DURATION_MILLIS,
|
||||
RETRY_LIMIT,
|
||||
totalResponseFailureWaitDuration,
|
||||
REQUEST_TIMEOUT);
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected static final ServerRequestPair
|
||||
HTTP1 = ServerRequestPair.of(Version.HTTP_1_1, false),
|
||||
HTTPS1 = ServerRequestPair.of(Version.HTTP_1_1, true),
|
||||
HTTP2 = ServerRequestPair.of(Version.HTTP_2, false),
|
||||
HTTPS2 = ServerRequestPair.of(Version.HTTP_2, true),
|
||||
HTTP3 = ServerRequestPair.of(Version.HTTP_3, true);
|
||||
|
||||
private static SSLContext createSslContext() {
|
||||
try {
|
||||
return new SimpleSSLContext().get();
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected record ServerRequestPair(HttpTestServer server, HttpRequest request, boolean secure) {
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
private static final CountDownLatch SHUT_DOWN_LATCH = new CountDownLatch(1);
|
||||
|
||||
private static final AtomicInteger SERVER_COUNTER = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* An arbitrary content length to cause the client wait for it.
|
||||
* It just needs to be greater than zero, and big enough to trigger a timeout when delivered slowly.
|
||||
*/
|
||||
public static final int CONTENT_LENGTH = 1234;
|
||||
|
||||
public enum ServerHandlerBehaviour {
|
||||
BLOCK_BEFORE_HEADER_DELIVERY,
|
||||
BLOCK_BEFORE_BODY_DELIVERY,
|
||||
DELIVER_BODY_SLOWLY,
|
||||
DELIVER_NO_BODY
|
||||
}
|
||||
|
||||
public static volatile ServerHandlerBehaviour SERVER_HANDLER_BEHAVIOUR;
|
||||
|
||||
public static volatile int SERVER_HANDLER_PENDING_FAILURE_COUNT = 0;
|
||||
|
||||
private static ServerRequestPair of(Version version, boolean secure) {
|
||||
|
||||
// Create the server and the request URI
|
||||
var sslContext = secure ? SSL_CONTEXT : null;
|
||||
var serverId = "" + SERVER_COUNTER.getAndIncrement();
|
||||
var server = createServer(version, sslContext);
|
||||
server.getVersion();
|
||||
var handlerPath = "/%s/".formatted(CLASS_NAME);
|
||||
var requestUriScheme = secure ? "https" : "http";
|
||||
var requestUri = URI.create("%s://%s%s-".formatted(requestUriScheme, server.serverAuthority(), handlerPath));
|
||||
|
||||
// Register the request handler
|
||||
server.addHandler(createServerHandler(serverId), handlerPath);
|
||||
|
||||
// Create the request
|
||||
var request = createRequestBuilder(requestUri, version).timeout(REQUEST_TIMEOUT).build();
|
||||
|
||||
// Create the pair
|
||||
var pair = new ServerRequestPair(server, request, secure);
|
||||
pair.server.start();
|
||||
LOGGER.log("Server[%s] is started at `%s`", serverId, server.serverAuthority());
|
||||
return pair;
|
||||
|
||||
}
|
||||
|
||||
private static HttpTestServer createServer(Version version, SSLContext sslContext) {
|
||||
try {
|
||||
return switch (version) {
|
||||
case HTTP_1_1, HTTP_2 -> HttpTestServer.create(version, sslContext, EXECUTOR);
|
||||
case HTTP_3 -> HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, EXECUTOR);
|
||||
};
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpTestHandler createServerHandler(String serverId) {
|
||||
return (exchange) -> {
|
||||
var connectionKey = exchange.getConnectionKey();
|
||||
LOGGER.log(
|
||||
"Server[%s] has received request %s",
|
||||
serverId, Map.of("connectionKey", connectionKey));
|
||||
try (exchange) {
|
||||
|
||||
// Short-circuit on `HEAD` requests.
|
||||
// They are used for admitting established connections to the pool.
|
||||
if ("HEAD".equals(exchange.getRequestMethod())) {
|
||||
LOGGER.log(
|
||||
"Server[%s] is responding to the `HEAD` request %s",
|
||||
serverId, Map.of("connectionKey", connectionKey));
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Short-circuit if instructed to fail
|
||||
synchronized (ServerRequestPair.class) {
|
||||
if (SERVER_HANDLER_PENDING_FAILURE_COUNT > 0) {
|
||||
LOGGER.log(
|
||||
"Server[%s] is prematurely failing as instructed %s",
|
||||
serverId,
|
||||
Map.of(
|
||||
"connectionKey", connectionKey,
|
||||
"SERVER_HANDLER_PENDING_FAILURE_COUNT", SERVER_HANDLER_PENDING_FAILURE_COUNT));
|
||||
// Closing the exchange will trigger an `END_STREAM` without a headers frame.
|
||||
// This is a protocol violation, hence we must reset the stream first.
|
||||
// We are doing so using by rejecting the stream, which is known to make the client retry.
|
||||
if (Version.HTTP_2.equals(exchange.getExchangeVersion())) {
|
||||
exchange.resetStream(ErrorFrame.REFUSED_STREAM);
|
||||
} else if (Version.HTTP_3.equals(exchange.getExchangeVersion())) {
|
||||
exchange.resetStream(Http3Error.H3_REQUEST_REJECTED.code());
|
||||
}
|
||||
SERVER_HANDLER_PENDING_FAILURE_COUNT--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (SERVER_HANDLER_BEHAVIOUR) {
|
||||
|
||||
case BLOCK_BEFORE_HEADER_DELIVERY -> sleepIndefinitely(serverId, connectionKey);
|
||||
|
||||
case BLOCK_BEFORE_BODY_DELIVERY -> {
|
||||
sendResponseHeaders(serverId, exchange, connectionKey);
|
||||
sleepIndefinitely(serverId, connectionKey);
|
||||
}
|
||||
|
||||
case DELIVER_BODY_SLOWLY -> {
|
||||
sendResponseHeaders(serverId, exchange, connectionKey);
|
||||
sendResponseBodySlowly(serverId, exchange, connectionKey);
|
||||
}
|
||||
|
||||
case DELIVER_NO_BODY -> sendResponseHeaders(serverId, exchange, connectionKey, 204, 0);
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception exception) {
|
||||
var message = String.format(
|
||||
"Server[%s] has failed! %s",
|
||||
serverId, Map.of("connectionKey", connectionKey));
|
||||
LOGGER.log(System.Logger.Level.ERROR, message, exception);
|
||||
if (exception instanceof InterruptedException) {
|
||||
// Restore the interrupt
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
throw new RuntimeException(message, exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void sleepIndefinitely(String serverId, String connectionKey) throws InterruptedException {
|
||||
LOGGER.log("Server[%s] is sleeping %s", serverId, Map.of("connectionKey", connectionKey));
|
||||
SHUT_DOWN_LATCH.await();
|
||||
}
|
||||
|
||||
private static void sendResponseHeaders(String serverId, HttpTestExchange exchange, String connectionKey)
|
||||
throws IOException {
|
||||
sendResponseHeaders(serverId, exchange, connectionKey, 200, CONTENT_LENGTH);
|
||||
}
|
||||
|
||||
private static void sendResponseHeaders(
|
||||
String serverId,
|
||||
HttpTestExchange exchange,
|
||||
String connectionKey,
|
||||
int statusCode,
|
||||
long contentLength)
|
||||
throws IOException {
|
||||
LOGGER.log("Server[%s] is sending headers %s", serverId, Map.of("connectionKey", connectionKey));
|
||||
exchange.sendResponseHeaders(statusCode, contentLength);
|
||||
// Force the headers to be flushed
|
||||
exchange.getResponseBody().flush();
|
||||
}
|
||||
|
||||
private static void sendResponseBodySlowly(String serverId, HttpTestExchange exchange, String connectionKey)
|
||||
throws Exception {
|
||||
var perBytePauseDuration = Duration.ofMillis(100);
|
||||
assertTrue(
|
||||
perBytePauseDuration.multipliedBy(CONTENT_LENGTH).compareTo(REQUEST_TIMEOUT) > 0,
|
||||
"Per-byte pause duration (%s) must be long enough to exceed the timeout (%s) when delivering the content (%s bytes)".formatted(
|
||||
perBytePauseDuration, REQUEST_TIMEOUT, CONTENT_LENGTH));
|
||||
try (var responseBody = exchange.getResponseBody()) {
|
||||
for (int i = 0; i < CONTENT_LENGTH; i++) {
|
||||
LOGGER.log(
|
||||
"Server[%s] is sending the body %s/%s %s",
|
||||
serverId, i, CONTENT_LENGTH, Map.of("connectionKey", connectionKey));
|
||||
responseBody.write(i);
|
||||
responseBody.flush();
|
||||
Thread.sleep(perBytePauseDuration);
|
||||
}
|
||||
throw new AssertionError("Delivery should never have succeeded due to timeout!");
|
||||
} catch (IOException _) {
|
||||
// Client's timeout mechanism is expected to short-circuit and cut the stream.
|
||||
// Hence, discard I/O failures.
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient createClientWithEstablishedConnection() throws IOException, InterruptedException {
|
||||
var version = server.getVersion();
|
||||
var client = createClientBuilderFor(version)
|
||||
.version(version)
|
||||
.sslContext(SSL_CONTEXT)
|
||||
.proxy(NO_PROXY)
|
||||
.build();
|
||||
// Ensure an established connection is admitted to the pool. This
|
||||
// helps to cross out any possibilities of a timeout before a
|
||||
// request makes it to the server handler. For instance, consider
|
||||
// HTTP/1.1 to HTTP/2 upgrades, or long-running TLS handshakes.
|
||||
var headRequest = createRequestBuilder(request.uri(), version).HEAD().build();
|
||||
client.send(headRequest, HttpResponse.BodyHandlers.discarding());
|
||||
return client;
|
||||
}
|
||||
|
||||
private static HttpRequest.Builder createRequestBuilder(URI uri, Version version) {
|
||||
var requestBuilder = HttpRequest.newBuilder(uri).version(version);
|
||||
if (Version.HTTP_3.equals(version)) {
|
||||
requestBuilder.setOption(HttpOption.H3_DISCOVERY, HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY);
|
||||
}
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
var version = server.getVersion();
|
||||
var versionString = version.toString();
|
||||
return switch (version) {
|
||||
case HTTP_1_1, HTTP_2 -> secure ? versionString.replaceFirst("_", "S_") : versionString;
|
||||
case HTTP_3 -> versionString;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void closeServers() {
|
||||
|
||||
// Terminate all handlers before shutting down the server, which would block otherwise.
|
||||
ServerRequestPair.SHUT_DOWN_LATCH.countDown();
|
||||
ServerRequestPair.EXECUTOR.shutdown();
|
||||
|
||||
// Shut down servers
|
||||
Exception[] exceptionRef = {null};
|
||||
serverRequestPairs()
|
||||
.forEach(pair -> {
|
||||
try {
|
||||
pair.server.stop();
|
||||
} catch (Exception exception) {
|
||||
if (exceptionRef[0] == null) {
|
||||
exceptionRef[0] = exception;
|
||||
} else {
|
||||
exceptionRef[0].addSuppressed(exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (exceptionRef[0] != null) {
|
||||
throw new RuntimeException("failed closing one or more server resources", exceptionRef[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures how many times the handler should fail.
|
||||
*/
|
||||
@BeforeEach
|
||||
void resetServerHandlerFailureIndex() {
|
||||
ServerRequestPair.SERVER_HANDLER_PENDING_FAILURE_COUNT = Math.max(0, RETRY_LIMIT - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the handler has failed as many times as instructed.
|
||||
*/
|
||||
@AfterEach
|
||||
void verifyServerHandlerFailureIndex() {
|
||||
assertEquals(0, ServerRequestPair.SERVER_HANDLER_PENDING_FAILURE_COUNT);
|
||||
}
|
||||
|
||||
protected static Stream<ServerRequestPair> serverRequestPairs() {
|
||||
return Stream.of(HTTP1, HTTPS1, HTTP2, HTTPS2, HTTP3);
|
||||
}
|
||||
|
||||
protected static void assertThrowsHttpTimeoutException(Executable executable) {
|
||||
var rootException = assertThrows(Exception.class, executable);
|
||||
// Due to intricacies involved in the way exceptions are generated and
|
||||
// nested, there is no bullet-proof way to determine at which level of
|
||||
// the causal chain an `HttpTimeoutException` will show up. Hence, we
|
||||
// scan through the entire causal chain.
|
||||
Throwable exception = rootException;
|
||||
while (exception != null) {
|
||||
if (exception instanceof HttpTimeoutException) {
|
||||
return;
|
||||
}
|
||||
exception = exception.getCause();
|
||||
}
|
||||
throw new AssertionError("was expecting an `HttpTimeoutException` in the causal chain", rootException);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2025, 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.
|
||||
*/
|
||||
package jdk.internal.net.http;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
|
||||
public enum HttpClientTimerAccess {;
|
||||
|
||||
public static void assertNoResponseTimerEventRegistrations(HttpClient client) {
|
||||
assertTimerEventRegistrationCount(client, ResponseTimerEvent.class, 0);
|
||||
}
|
||||
|
||||
private static void assertTimerEventRegistrationCount(
|
||||
HttpClient client,
|
||||
Class<? extends TimeoutEvent> clazz,
|
||||
long expectedCount) {
|
||||
var facade = assertType(HttpClientFacade.class, client);
|
||||
var actualCount = facade.impl.timers().stream().filter(clazz::isInstance).count();
|
||||
if (actualCount != 0) {
|
||||
throw new AssertionError(
|
||||
"Found %s occurrences of `%s` timer event registrations while expecting %s.".formatted(
|
||||
actualCount, clazz.getCanonicalName(), expectedCount));
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T assertType(Class<T> expectedType, Object instance) {
|
||||
if (!expectedType.isInstance(instance)) {
|
||||
var expectedTypeName = expectedType.getCanonicalName();
|
||||
var actualTypeName = instance != null ? instance.getClass().getCanonicalName() : null;
|
||||
throw new AssertionError(
|
||||
"Was expecting an instance of type `%s`, found: `%s`".formatted(
|
||||
expectedTypeName, actualTypeName));
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
T typedInstance = (T) instance;
|
||||
return typedInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2018, 2025, 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
|
||||
@@ -23,8 +23,10 @@
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8217429
|
||||
* @bug 8217429 8208693
|
||||
* @library ../access
|
||||
* @build DummyWebSocketServer
|
||||
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
|
||||
* @run testng/othervm
|
||||
* WebSocketTest
|
||||
*/
|
||||
@@ -40,6 +42,7 @@ import java.net.http.WebSocket;
|
||||
import java.net.http.WebSocketHandshakeException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
@@ -48,6 +51,7 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
@@ -58,6 +62,7 @@ import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpClient.newBuilder;
|
||||
import static java.net.http.WebSocket.NORMAL_CLOSURE;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static jdk.internal.net.http.HttpClientTimerAccess.assertNoResponseTimerEventRegistrations;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
import static org.testng.Assert.fail;
|
||||
@@ -143,6 +148,45 @@ public class WebSocketTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the internally issued request to establish the WebSocket
|
||||
* connection does not leave any response timers registered at the client
|
||||
* after the WebSocket handshake.
|
||||
*/
|
||||
@Test
|
||||
public void responseTimerCleanUp() throws Exception {
|
||||
try (var server = new DummyWebSocketServer()) {
|
||||
server.open();
|
||||
try (var client = newBuilder().proxy(NO_PROXY).build()) {
|
||||
var connectionEstablished = new CountDownLatch(1);
|
||||
var webSocketListener = new WebSocket.Listener() {
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket) {
|
||||
connectionEstablished.countDown();
|
||||
}
|
||||
|
||||
};
|
||||
var webSocket = client
|
||||
.newWebSocketBuilder()
|
||||
// Explicitly configure a timeout to get a response
|
||||
// timer event get registered at the client. The query
|
||||
// should succeed without timing out.
|
||||
.connectTimeout(Duration.ofMinutes(2))
|
||||
.buildAsync(server.getURI(), webSocketListener)
|
||||
.join();
|
||||
try {
|
||||
connectionEstablished.await();
|
||||
// We expect the response timer event to get evicted once
|
||||
// the WebSocket handshake headers are received.
|
||||
assertNoResponseTimerEventRegistrations(client);
|
||||
} finally {
|
||||
webSocket.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialBinaryThenText() throws IOException {
|
||||
try (var server = new DummyWebSocketServer()) {
|
||||
|
||||
Reference in New Issue
Block a user