Skip to content

Commit 16699a3

Browse files
committed
8208693: HttpClient: Extend the request timeout's scope to cover the response body
Reviewed-by: jpai, dfuchs
1 parent 14000a2 commit 16699a3

File tree

15 files changed

+1088
-25
lines changed

15 files changed

+1088
-25
lines changed

src/java.net.http/share/classes/java/net/http/HttpClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,22 @@ public interface Builder {
312312
* need to be established, for example if a connection can be reused
313313
* from a previous request, then this timeout duration has no effect.
314314
*
315+
* @implSpec
316+
* A connection timeout applies to the entire connection phase, from the
317+
* moment a connection is requested until it is established.
318+
* Implementations are recommended to ensure that the connection timeout
319+
* covers any SSL/TLS handshakes.
320+
*
321+
* @implNote
322+
* The built-in JDK implementation of the connection timeout covers any
323+
* SSL/TLS handshakes.
324+
*
315325
* @param duration the duration to allow the underlying connection to be
316326
* established
317327
* @return this builder
318328
* @throws IllegalArgumentException if the duration is non-positive
329+
* @see HttpRequest.Builder#timeout(Duration) Configuring timeout for
330+
* request execution
319331
*/
320332
public Builder connectTimeout(Duration duration);
321333

src/java.net.http/share/classes/java/net/http/HttpRequest.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,28 @@ public interface Builder {
258258
* {@link HttpClient#sendAsync(java.net.http.HttpRequest,
259259
* java.net.http.HttpResponse.BodyHandler) HttpClient::sendAsync}
260260
* completes exceptionally with an {@code HttpTimeoutException}. The effect
261-
* of not setting a timeout is the same as setting an infinite Duration,
262-
* i.e. block forever.
261+
* of not setting a timeout is the same as setting an infinite
262+
* {@code Duration}, i.e., block forever.
263+
*
264+
* @implSpec
265+
* A timeout applies to the duration measured from the instant the
266+
* request execution starts to, <em>at least</em>, the instant an
267+
* {@link HttpResponse} is constructed. The elapsed time includes
268+
* obtaining a connection for transport and retrieving the response
269+
* headers.
270+
*
271+
* @implNote
272+
* The JDK built-in implementation applies timeout over the duration
273+
* measured from the instant the request execution starts to <b>the
274+
* instant the response body is consumed</b>, if present. This is
275+
* implemented by stopping the timer after the response body subscriber
276+
* completion.
263277
*
264278
* @param duration the timeout duration
265279
* @return this builder
266280
* @throws IllegalArgumentException if the duration is non-positive
281+
* @see HttpClient.Builder#connectTimeout(Duration) Configuring
282+
* timeout for connection establishment
267283
*/
268284
public abstract Builder timeout(Duration duration);
269285

src/java.net.http/share/classes/java/net/http/WebSocket.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -144,6 +144,16 @@ interface Builder {
144144
* {@link HttpTimeoutException}. If this method is not invoked then the
145145
* infinite timeout is assumed.
146146
*
147+
* @implSpec
148+
* A connection timeout applies to the entire connection phase, from the
149+
* moment a connection is requested until it is established.
150+
* Implementations are recommended to ensure that the connection timeout
151+
* covers any WebSocket and SSL/TLS handshakes.
152+
*
153+
* @implNote
154+
* The built-in JDK implementation of the connection timeout covers any
155+
* WebSocket and SSL/TLS handshakes.
156+
*
147157
* @param timeout
148158
* the timeout, non-{@linkplain Duration#isNegative() negative},
149159
* non-{@linkplain Duration#ZERO ZERO}

src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,18 @@ void nullBody(HttpResponse<T> resp, Throwable t) {
581581
// Needed for HTTP/2 to subscribe a dummy subscriber and close the stream
582582
}
583583

584+
/**
585+
* {@return {@code true}, if it is allowed to cancel the request timer on
586+
* response body subscriber termination; {@code false}, otherwise}
587+
*
588+
* @param webSocket indicates if the associated request is a WebSocket handshake
589+
* @param statusCode the status code of the associated response
590+
*/
591+
static boolean cancelTimerOnResponseBodySubscriberTermination(
592+
boolean webSocket, int statusCode) {
593+
return webSocket || statusCode < 100 || statusCode >= 200;
594+
}
595+
584596
/* The following methods have separate HTTP/1.1 and HTTP/2 implementations */
585597

586598
abstract CompletableFuture<ExchangeImpl<T>> sendHeadersAsync();

src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,15 @@ private void error() {
206206
*/
207207
static final class Http1ResponseBodySubscriber<U> extends HttpBodySubscriberWrapper<U> {
208208
final Http1Exchange<U> exchange;
209-
Http1ResponseBodySubscriber(BodySubscriber<U> userSubscriber, Http1Exchange<U> exchange) {
209+
210+
private final boolean cancelTimerOnTermination;
211+
212+
Http1ResponseBodySubscriber(
213+
BodySubscriber<U> userSubscriber,
214+
boolean cancelTimerOnTermination,
215+
Http1Exchange<U> exchange) {
210216
super(userSubscriber);
217+
this.cancelTimerOnTermination = cancelTimerOnTermination;
211218
this.exchange = exchange;
212219
}
213220

@@ -220,6 +227,14 @@ protected void register() {
220227
protected void unregister() {
221228
exchange.unregisterResponseSubscriber(this);
222229
}
230+
231+
@Override
232+
protected void onTermination() {
233+
if (cancelTimerOnTermination) {
234+
exchange.exchange.multi.cancelTimer();
235+
}
236+
}
237+
223238
}
224239

225240
@Override
@@ -459,9 +474,10 @@ CompletableFuture<T> readBodyAsync(BodyHandler<T> handler,
459474
@Override
460475
Http1ResponseBodySubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
461476
BodySubscriber<T> subscriber = handler.apply(response);
462-
Http1ResponseBodySubscriber<T> bs =
463-
new Http1ResponseBodySubscriber<T>(subscriber, this);
464-
return bs;
477+
var cancelTimerOnTermination =
478+
cancelTimerOnResponseBodySubscriberTermination(
479+
exchange.request().isWebSocket(), response.statusCode());
480+
return new Http1ResponseBodySubscriber<>(subscriber, cancelTimerOnTermination, this);
465481
}
466482

467483
@Override

src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,12 @@ private void unregisterResponseSubscriber(Http3StreamResponseSubscriber<?> subsc
554554
}
555555

556556
final class Http3StreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {
557-
Http3StreamResponseSubscriber(BodySubscriber<U> subscriber) {
557+
558+
private final boolean cancelTimerOnTermination;
559+
560+
Http3StreamResponseSubscriber(BodySubscriber<U> subscriber, boolean cancelTimerOnTermination) {
558561
super(subscriber);
562+
this.cancelTimerOnTermination = cancelTimerOnTermination;
559563
}
560564

561565
@Override
@@ -568,6 +572,13 @@ protected void register() {
568572
registerResponseSubscriber(this);
569573
}
570574

575+
@Override
576+
protected void onTermination() {
577+
if (cancelTimerOnTermination) {
578+
exchange.multi.cancelTimer();
579+
}
580+
}
581+
571582
@Override
572583
protected void logComplete(Throwable error) {
573584
if (error == null) {
@@ -590,9 +601,10 @@ protected void logComplete(Throwable error) {
590601
Http3StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler,
591602
ResponseInfo response) {
592603
if (debug.on()) debug.log("Creating body subscriber");
593-
Http3StreamResponseSubscriber<T> subscriber =
594-
new Http3StreamResponseSubscriber<>(handler.apply(response));
595-
return subscriber;
604+
var cancelTimerOnTermination =
605+
cancelTimerOnResponseBodySubscriberTermination(
606+
exchange.request().isWebSocket(), response.statusCode());
607+
return new Http3StreamResponseSubscriber<>(handler.apply(response), cancelTimerOnTermination);
596608
}
597609

598610
@Override

src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,6 +1880,13 @@ void cancelTimer(TimeoutEvent event) {
18801880
}
18811881
}
18821882

1883+
// Visible for tests
1884+
List<TimeoutEvent> timers() {
1885+
synchronized (this) {
1886+
return new ArrayList<>(timeouts);
1887+
}
1888+
}
1889+
18831890
/**
18841891
* Purges ( handles ) timer events that have passed their deadline, and
18851892
* returns the amount of time, in milliseconds, until the next earliest

src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
package jdk.internal.net.http;
2727

28-
import java.io.IOError;
2928
import java.io.IOException;
3029
import java.lang.ref.WeakReference;
3130
import java.net.ConnectException;
@@ -254,7 +253,7 @@ public Optional<Duration> remainingConnectTimeout() {
254253
.map(ConnectTimeoutTracker::getRemaining);
255254
}
256255

257-
private void cancelTimer() {
256+
void cancelTimer() {
258257
if (responseTimerEvent != null) {
259258
client.cancelTimer(responseTimerEvent);
260259
responseTimerEvent = null;
@@ -404,6 +403,8 @@ private HttpResponse<T> setNewResponse(HttpRequest request, Response r, T body,
404403
processAltSvcHeader(r, client(), currentreq);
405404
Exchange<T> exch = getExchange();
406405
if (bodyNotPermitted(r)) {
406+
// No response body consumption is expected, we can cancel the timer right away
407+
cancelTimer();
407408
if (bodyIsPresent(r)) {
408409
IOException ioe = new IOException(
409410
"unexpected content length header with 204 response");
@@ -467,6 +468,8 @@ private CompletableFuture<Response> retryRequest() {
467468

468469
private CompletableFuture<Response> responseAsyncImpl(final boolean applyReqFilters) {
469470
if (currentreq.timeout().isPresent()) {
471+
// Retried/Forwarded requests should reset the timer, if present
472+
cancelTimer();
470473
responseTimerEvent = ResponseTimerEvent.of(this);
471474
client.registerTimer(responseTimerEvent);
472475
}
@@ -502,7 +505,6 @@ private CompletableFuture<Response> responseAsyncImpl(final boolean applyReqFilt
502505
}
503506
return completedFuture(response);
504507
} else {
505-
cancelTimer();
506508
setNewResponse(currentreq, response, null, exch);
507509
if (currentreq.isWebSocket()) {
508510
// need to close the connection and open a new one.
@@ -520,11 +522,18 @@ private CompletableFuture<Response> responseAsyncImpl(final boolean applyReqFilt
520522
} })
521523
.handle((response, ex) -> {
522524
// 5. handle errors and cancel any timer set
523-
cancelTimer();
524525
if (ex == null) {
525526
assert response != null;
526527
return completedFuture(response);
527528
}
529+
530+
// Cancel the timer. Note that we only do so if the
531+
// response has completed exceptionally. That is, we don't
532+
// cancel the timer if there are no exceptions, since the
533+
// response body might still get consumed, and it is
534+
// still subject to the response timer.
535+
cancelTimer();
536+
528537
// all exceptions thrown are handled here
529538
final RetryContext retryCtx = checkRetryEligible(ex, exch);
530539
assert retryCtx != null : "retry context is null";

src/java.net.http/share/classes/jdk/internal/net/http/Stream.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,10 @@ CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
390390

391391
@Override
392392
Http2StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
393-
Http2StreamResponseSubscriber<T> subscriber =
394-
new Http2StreamResponseSubscriber<>(handler.apply(response));
395-
return subscriber;
393+
var cancelTimerOnTermination =
394+
cancelTimerOnResponseBodySubscriberTermination(
395+
exchange.request().isWebSocket(), response.statusCode());
396+
return new Http2StreamResponseSubscriber<>(handler.apply(response), cancelTimerOnTermination);
396397
}
397398

398399
// The Http2StreamResponseSubscriber is registered with the HttpClient
@@ -1694,6 +1695,11 @@ CompletableFuture<T> readBodyAsync(
16941695
.whenComplete((v, t) -> pushGroup.pushError(t));
16951696
}
16961697

1698+
@Override
1699+
Http2StreamResponseSubscriber<T> createResponseSubscriber(BodyHandler<T> handler, ResponseInfo response) {
1700+
return new Http2StreamResponseSubscriber<T>(handler.apply(response), false);
1701+
}
1702+
16971703
@Override
16981704
void completeResponse(Response r) {
16991705
Log.logResponse(r::toString);
@@ -1924,8 +1930,12 @@ public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws
19241930
}
19251931

19261932
final class Http2StreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {
1927-
Http2StreamResponseSubscriber(BodySubscriber<U> subscriber) {
1933+
1934+
private final boolean cancelTimerOnTermination;
1935+
1936+
Http2StreamResponseSubscriber(BodySubscriber<U> subscriber, boolean cancelTimerOnTermination) {
19281937
super(subscriber);
1938+
this.cancelTimerOnTermination = cancelTimerOnTermination;
19291939
}
19301940

19311941
@Override
@@ -1938,6 +1948,13 @@ protected void unregister() {
19381948
unregisterResponseSubscriber(this);
19391949
}
19401950

1951+
@Override
1952+
protected void onTermination() {
1953+
if (cancelTimerOnTermination) {
1954+
exchange.multi.cancelTimer();
1955+
}
1956+
}
1957+
19411958
}
19421959

19431960
private static final VarHandle DEREGISTERED;

src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -33,7 +33,6 @@
3333
import java.util.concurrent.CompletionStage;
3434
import java.util.concurrent.Flow;
3535
import java.util.concurrent.Flow.Subscription;
36-
import java.util.concurrent.atomic.AtomicBoolean;
3736
import java.util.concurrent.atomic.AtomicLong;
3837
import java.util.concurrent.locks.ReentrantLock;
3938

@@ -51,7 +50,6 @@ public class HttpBodySubscriberWrapper<T> implements TrustedSubscriber<T> {
5150
public static final Comparator<HttpBodySubscriberWrapper<?>> COMPARE_BY_ID
5251
= Comparator.comparing(HttpBodySubscriberWrapper::id);
5352

54-
5553
public static final Flow.Subscription NOP = new Flow.Subscription() {
5654
@Override
5755
public void request(long n) { }
@@ -75,7 +73,18 @@ public HttpBodySubscriberWrapper(BodySubscriber<T> userSubscriber) {
7573
this.userSubscriber = userSubscriber;
7674
}
7775

78-
private class SubscriptionWrapper implements Subscription {
76+
/**
77+
* A callback to be invoked <em>before</em> termination, whether due to the
78+
* completion or failure of the subscriber, or cancellation of the
79+
* subscription. To be precise, this method is called before
80+
* {@link #onComplete()}, {@link #onError(Throwable) onError()}, or
81+
* {@link #onCancel()}.
82+
*/
83+
protected void onTermination() {
84+
// Do nothing
85+
}
86+
87+
private final class SubscriptionWrapper implements Subscription {
7988
final Subscription subscription;
8089
SubscriptionWrapper(Subscription s) {
8190
this.subscription = Objects.requireNonNull(s);
@@ -92,6 +101,7 @@ public void cancel() {
92101
subscription.cancel();
93102
} finally {
94103
if (markCancelled()) {
104+
onTermination();
95105
onCancel();
96106
}
97107
}
@@ -284,6 +294,7 @@ protected void onCancel() {
284294
*/
285295
public final void complete(Throwable t) {
286296
if (markCompleted()) {
297+
onTermination();
287298
logComplete(t);
288299
tryUnregister();
289300
t = withError = Utils.getCompletionCause(t);

0 commit comments

Comments
 (0)