From dbf0742bf205ec57477373ebd43016383f7e7791 Mon Sep 17 00:00:00 2001
From: Ashutosh Mehra
Date: Thu, 4 Dec 2025 05:03:07 +0000
Subject: [PATCH 001/413] 8373046: Method::get_c2i_unverified_entry() and
get_c2i_no_clinit_check_entry() are missing check for abstract method
Reviewed-by: kvn, vlivanov
---
src/hotspot/share/oops/method.cpp | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/hotspot/share/oops/method.cpp b/src/hotspot/share/oops/method.cpp
index cb1c8ea37e8..1a2e5f0bee4 100644
--- a/src/hotspot/share/oops/method.cpp
+++ b/src/hotspot/share/oops/method.cpp
@@ -168,11 +168,17 @@ address Method::get_c2i_entry() {
}
address Method::get_c2i_unverified_entry() {
+ if (is_abstract()) {
+ return SharedRuntime::get_handle_wrong_method_abstract_stub();
+ }
assert(adapter() != nullptr, "must have");
return adapter()->get_c2i_unverified_entry();
}
address Method::get_c2i_no_clinit_check_entry() {
+ if (is_abstract()) {
+ return nullptr;
+ }
assert(VM_Version::supports_fast_class_init_checks(), "");
assert(adapter() != nullptr, "must have");
return adapter()->get_c2i_no_clinit_check_entry();
From 828498c54b3b1089af9e076cb45f3cf3bea58e2f Mon Sep 17 00:00:00 2001
From: SendaoYan
Date: Thu, 4 Dec 2025 07:34:43 +0000
Subject: [PATCH 002/413] 8371978: tools/jar/ReproducibleJar.java fails on XFS
Reviewed-by: jpai
---
test/jdk/tools/jar/ReproducibleJar.java | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/test/jdk/tools/jar/ReproducibleJar.java b/test/jdk/tools/jar/ReproducibleJar.java
index ed5e2ed2ae3..5f59d1cbe41 100644
--- a/test/jdk/tools/jar/ReproducibleJar.java
+++ b/test/jdk/tools/jar/ReproducibleJar.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 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
@@ -66,7 +66,9 @@ public class ReproducibleJar {
private static final TimeZone TZ = TimeZone.getDefault();
private static final boolean DST = TZ.inDaylightTime(new Date());
private static final String UNIX_2038_ROLLOVER_TIME = "2038-01-19T03:14:07Z";
+ private static final String UNIX_EPOCH_TIME = "1970-01-01T00:00:00Z";
private static final Instant UNIX_2038_ROLLOVER = Instant.parse(UNIX_2038_ROLLOVER_TIME);
+ private static final Instant UNIX_EPOCH = Instant.parse(UNIX_EPOCH_TIME);
private static final File DIR_OUTER = new File("outer");
private static final File DIR_INNER = new File(DIR_OUTER, "inner");
private static final File FILE_INNER = new File(DIR_INNER, "foo.txt");
@@ -231,12 +233,15 @@ public class ReproducibleJar {
if (Math.abs(now - original) > PRECISION) {
// If original time is after UNIX 2038 32bit rollover
- // and the now time is exactly the rollover time, then assume
+ // and the now time is exactly the rollover time or UNIX epoch time, then assume
// running on a file system that only supports to 2038 (e.g.XFS) and pass test
- if (FileTime.fromMillis(original).toInstant().isAfter(UNIX_2038_ROLLOVER) &&
- FileTime.fromMillis(now).toInstant().equals(UNIX_2038_ROLLOVER)) {
- System.out.println("Checking file time after Unix 2038 rollover," +
- " and extracted file time is " + UNIX_2038_ROLLOVER_TIME + ", " +
+ Instant originalInstant = FileTime.fromMillis(original).toInstant();
+ Instant nowInstant = FileTime.fromMillis(now).toInstant();
+ if (originalInstant.isAfter(UNIX_2038_ROLLOVER) &&
+ (nowInstant.equals(UNIX_2038_ROLLOVER) ||
+ nowInstant.equals(UNIX_EPOCH))) {
+ System.out.println("Checking file time after Unix 2038 rollover," +
+ " and extracted file time is " + nowInstant + ", " +
" Assuming restricted file system, pass file time check.");
} else {
throw new AssertionError("checkFileTime failed," +
From 63a10e0099111d69b167abf99d1a00084c4d6c1e Mon Sep 17 00:00:00 2001
From: Erik Gahlin
Date: Thu, 4 Dec 2025 08:01:17 +0000
Subject: [PATCH 003/413] 8373024: JFR: CPU throttle rate can't handle
incorrect values
Reviewed-by: mgronlun
---
.../share/classes/jdk/jfr/internal/PlatformEventType.java | 3 ++-
.../classes/jdk/jfr/internal/settings/CPUThrottleSetting.java | 2 +-
src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java
index eaba86e6327..1180ebd6ea2 100644
--- a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java
@@ -34,6 +34,7 @@ import jdk.jfr.internal.periodic.PeriodicEvents;
import jdk.jfr.internal.util.ImplicitFields;
import jdk.jfr.internal.util.TimespanRate;
import jdk.jfr.internal.util.Utils;
+import jdk.jfr.internal.settings.CPUThrottleSetting;
import jdk.jfr.internal.settings.Throttler;
import jdk.jfr.internal.tracing.Modification;
@@ -60,7 +61,7 @@ public final class PlatformEventType extends Type {
private boolean stackTraceEnabled = true;
private long thresholdTicks = 0;
private long period = 0;
- private TimespanRate cpuRate;
+ private TimespanRate cpuRate = TimespanRate.of(CPUThrottleSetting.DEFAULT_VALUE);
private boolean hasHook;
private boolean beginChunk;
diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java
index 944827f6d6f..7ea0ace21bb 100644
--- a/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/settings/CPUThrottleSetting.java
@@ -65,7 +65,7 @@ public final class CPUThrottleSetting extends SettingControl {
}
}
}
- return Objects.requireNonNullElse(highestRate.toString(), DEFAULT_VALUE);
+ return highestRate == null ? DEFAULT_VALUE : highestRate.toString();
}
@Override
diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java
index 2632cd63848..0a7b14965cb 100644
--- a/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/util/Rate.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 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
@@ -34,7 +34,7 @@ public record Rate(long amount, TimespanUnit unit) {
String value = splitted[0].strip();
String unit = splitted[1].strip();
TimespanUnit tu = TimespanUnit.fromText(unit);
- if (unit == null) {
+ if (tu == null) {
return null;
}
try {
From 771253e285c48329a9b45dfaaa852b64e74b31d4 Mon Sep 17 00:00:00 2001
From: Frederic Thevenet
Date: Thu, 4 Dec 2025 08:23:33 +0000
Subject: [PATCH 004/413] 8372802: PrintFlagsFinal should also print locked
flags
Reviewed-by: dholmes, stuefe, lmesnik
---
src/hotspot/share/runtime/flags/jvmFlag.cpp | 2 +-
.../runtime/CommandLine/PrintAllFlags.java | 69 +++++++++++++++++++
2 files changed, 70 insertions(+), 1 deletion(-)
create mode 100644 test/hotspot/jtreg/runtime/CommandLine/PrintAllFlags.java
diff --git a/src/hotspot/share/runtime/flags/jvmFlag.cpp b/src/hotspot/share/runtime/flags/jvmFlag.cpp
index 51517fa49db..405b47e1813 100644
--- a/src/hotspot/share/runtime/flags/jvmFlag.cpp
+++ b/src/hotspot/share/runtime/flags/jvmFlag.cpp
@@ -711,7 +711,7 @@ void JVMFlag::printFlags(outputStream* out, bool withComments, bool printRanges,
for (size_t i = 0; i < length; i++) {
const bool skip = (skipDefaults && flagTable[i].is_default());
const bool visited = iteratorMarkers.at(i);
- if (!visited && flagTable[i].is_unlocked() && !skip) {
+ if (!visited && !skip) {
if ((bestFlag == nullptr) || (strcmp(bestFlag->name(), flagTable[i].name()) > 0)) {
bestFlag = &flagTable[i];
bestFlagIndex = i;
diff --git a/test/hotspot/jtreg/runtime/CommandLine/PrintAllFlags.java b/test/hotspot/jtreg/runtime/CommandLine/PrintAllFlags.java
new file mode 100644
index 00000000000..13b0dd70d4e
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/CommandLine/PrintAllFlags.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2025, IBM Corporation.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8372802
+ * @summary Test that +PrintFlagsFinal print the same options when +UnlockExperimentalVMOptions and
+ * +UnlockDiagnosticVMOptions are set than when they aren't.
+ * @requires vm.flagless
+ * @library /test/lib
+ * @run driver PrintAllFlags
+ */
+
+import jdk.test.lib.process.ProcessTools;
+import jdk.test.lib.process.OutputAnalyzer;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class PrintAllFlags {
+ private static final Pattern optPattern = Pattern.compile("\\s*\\w+\\s\\w+\\s+=");
+
+ public static void main(String args[]) throws Exception {
+ var flagsFinal = runAndMakeVMOptionSet("-XX:+PrintFlagsFinal", "-version");
+ var flagsFinalUnlocked = runAndMakeVMOptionSet(
+ "-XX:+UnlockExperimentalVMOptions", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintFlagsFinal", "-version");
+ if (!flagsFinal.equals(flagsFinalUnlocked)) {
+ throw new RuntimeException("+PrintFlagsFinal should produce the same output" +
+ " whether or not UnlockExperimentalVMOptions and UnlockDiagnosticVMOptions are set");
+ }
+ }
+
+ private static Set runAndMakeVMOptionSet(String... args) throws IOException {
+ var output = new OutputAnalyzer(ProcessTools.createLimitedTestJavaProcessBuilder(args).start());
+ Set optNameSet = output.asLines().stream()
+ .map(optPattern::matcher)
+ .filter(Matcher::find)
+ .map(Matcher::group)
+ .collect(Collectors.toSet());
+ if (optNameSet.isEmpty()) {
+ throw new RuntimeException("Sanity test failed: no match for option pattern in process output");
+ }
+ return optNameSet;
+ }
+
+}
From bb867ed23e2d6394d7e7dab55cf2122889fdf3ac Mon Sep 17 00:00:00 2001
From: Kim Barrett
Date: Thu, 4 Dec 2025 08:32:00 +0000
Subject: [PATCH 005/413] 8372938: Fix reference to DeferredStatic in
HotSpot Style Guide
Reviewed-by: stefank, jsjolen
---
doc/hotspot-style.html | 4 ++--
doc/hotspot-style.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html
index a2ffb57e5a3..362245cd00a 100644
--- a/doc/hotspot-style.html
+++ b/doc/hotspot-style.html
@@ -1037,8 +1037,8 @@ running destructors at exit can lead to problems.
Some of the approaches used in HotSpot to avoid dynamic
initialization include:
-
Use the Deferred<T> class template. Add a call
-to its initialization function at an appropriate place during VM
+
Use the DeferredStatic<T> class template. Add
+a call to its initialization function at an appropriate place during VM
initialization. The underlying object is never destroyed.
For objects of class type, use a variable whose value is a
pointer to the class, initialized to nullptr. Provide an
diff --git a/doc/hotspot-style.md b/doc/hotspot-style.md
index c8f0f72b814..26549e3ca02 100644
--- a/doc/hotspot-style.md
+++ b/doc/hotspot-style.md
@@ -954,7 +954,7 @@ destructors at exit can lead to problems.
Some of the approaches used in HotSpot to avoid dynamic initialization
include:
-* Use the `Deferred` class template. Add a call to its initialization
+* Use the `DeferredStatic` class template. Add a call to its initialization
function at an appropriate place during VM initialization. The underlying
object is never destroyed.
From 317daa3c004fbb1738e0af6acfbaf50c403c8230 Mon Sep 17 00:00:00 2001
From: Matthias Baesken
Date: Thu, 4 Dec 2025 08:36:00 +0000
Subject: [PATCH 006/413] 8372643: Warning message on macos when building the
JDK - (arm64) /tmp/lto.o unable to open object file: No such file or
directory
Reviewed-by: erikj
---
make/common/native/Flags.gmk | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/make/common/native/Flags.gmk b/make/common/native/Flags.gmk
index 843701cb4db..efb4c08e74c 100644
--- a/make/common/native/Flags.gmk
+++ b/make/common/native/Flags.gmk
@@ -229,6 +229,11 @@ define SetupLinkerFlags
# TOOLCHAIN_TYPE plus OPENJDK_TARGET_OS
ifeq ($$($1_LINK_TIME_OPTIMIZATION), true)
$1_EXTRA_LDFLAGS += $(LDFLAGS_LTO)
+ # Instruct the ld64 linker not to delete the temporary object file
+ # generated during Link Time Optimization
+ ifeq ($(call isTargetOs, macosx), true)
+ $1_EXTRA_LDFLAGS += -Wl,-object_path_lto,$$($1_OBJECT_DIR)/$$($1_NAME)_lto_helper.o
+ endif
endif
$1_EXTRA_LDFLAGS += $$($1_LDFLAGS_$(OPENJDK_TARGET_OS_TYPE)) $$($1_LDFLAGS_$(OPENJDK_TARGET_OS)) \
From 14000a25e6efcbe55171d4cc8c68170a8cf0406f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joel=20Sikstr=C3=B6m?=
Date: Thu, 4 Dec 2025 09:37:56 +0000
Subject: [PATCH 007/413] 8373080: Parallel:
gc/arguments/TestMinInitialErgonomics.java should not be run with Large Pages
Reviewed-by: ayang, aboldtch
---
test/hotspot/jtreg/gc/arguments/TestMinInitialErgonomics.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/hotspot/jtreg/gc/arguments/TestMinInitialErgonomics.java b/test/hotspot/jtreg/gc/arguments/TestMinInitialErgonomics.java
index 6912499e53f..38eb07139bd 100644
--- a/test/hotspot/jtreg/gc/arguments/TestMinInitialErgonomics.java
+++ b/test/hotspot/jtreg/gc/arguments/TestMinInitialErgonomics.java
@@ -28,6 +28,7 @@ package gc.arguments;
* @bug 8006088
* @requires vm.gc.Parallel
* @requires vm.compMode != "Xcomp"
+ * @requires !vm.opt.final.UseLargePages
* @summary Test Parallel GC ergonomics decisions related to minimum and initial heap size.
* @library /test/lib
* @library /
From 16699a394d4d6c2b8a21e7de3c3d344c5a3309b4 Mon Sep 17 00:00:00 2001
From: Volkan Yazici
Date: Thu, 4 Dec 2025 09:40:31 +0000
Subject: [PATCH 008/413] 8208693: HttpClient: Extend the request timeout's
scope to cover the response body
Reviewed-by: jpai, dfuchs
---
.../classes/java/net/http/HttpClient.java | 12 +
.../classes/java/net/http/HttpRequest.java | 20 +-
.../classes/java/net/http/WebSocket.java | 12 +-
.../jdk/internal/net/http/ExchangeImpl.java | 12 +
.../jdk/internal/net/http/Http1Exchange.java | 24 +-
.../internal/net/http/Http3ExchangeImpl.java | 20 +-
.../jdk/internal/net/http/HttpClientImpl.java | 7 +
.../jdk/internal/net/http/MultiExchange.java | 17 +-
.../classes/jdk/internal/net/http/Stream.java | 25 +-
.../common/HttpBodySubscriberWrapper.java | 19 +-
.../httpclient/TimeoutResponseBodyTest.java | 285 ++++++++++++
.../httpclient/TimeoutResponseHeaderTest.java | 138 ++++++
.../TimeoutResponseTestSupport.java | 415 ++++++++++++++++++
.../net/http/HttpClientTimerAccess.java | 59 +++
.../httpclient/websocket/WebSocketTest.java | 48 +-
15 files changed, 1088 insertions(+), 25 deletions(-)
create mode 100644 test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java
create mode 100644 test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java
create mode 100644 test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java
create mode 100644 test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/HttpClientTimerAccess.java
diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java
index a7a2171857d..9ecb048342b 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpClient.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java
@@ -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);
diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequest.java b/src/java.net.http/share/classes/java/net/http/HttpRequest.java
index c56328ba4b4..741573e06b3 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java
@@ -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, at least, 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 the
+ * instant the response body is consumed, 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);
diff --git a/src/java.net.http/share/classes/java/net/http/WebSocket.java b/src/java.net.http/share/classes/java/net/http/WebSocket.java
index 313847cf449..84fc8472eef 100644
--- a/src/java.net.http/share/classes/java/net/http/WebSocket.java
+++ b/src/java.net.http/share/classes/java/net/http/WebSocket.java
@@ -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}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
index 74600e78557..c1ed01ff07a 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java
@@ -581,6 +581,18 @@ abstract class ExchangeImpl {
// 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> sendHeadersAsync();
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
index 02ce63b6314..72a47eca42c 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java
@@ -206,8 +206,15 @@ class Http1Exchange extends ExchangeImpl {
*/
static final class Http1ResponseBodySubscriber extends HttpBodySubscriberWrapper {
final Http1Exchange exchange;
- Http1ResponseBodySubscriber(BodySubscriber userSubscriber, Http1Exchange exchange) {
+
+ private final boolean cancelTimerOnTermination;
+
+ Http1ResponseBodySubscriber(
+ BodySubscriber userSubscriber,
+ boolean cancelTimerOnTermination,
+ Http1Exchange exchange) {
super(userSubscriber);
+ this.cancelTimerOnTermination = cancelTimerOnTermination;
this.exchange = exchange;
}
@@ -220,6 +227,14 @@ class Http1Exchange extends ExchangeImpl {
protected void unregister() {
exchange.unregisterResponseSubscriber(this);
}
+
+ @Override
+ protected void onTermination() {
+ if (cancelTimerOnTermination) {
+ exchange.exchange.multi.cancelTimer();
+ }
+ }
+
}
@Override
@@ -459,9 +474,10 @@ class Http1Exchange extends ExchangeImpl {
@Override
Http1ResponseBodySubscriber createResponseSubscriber(BodyHandler handler, ResponseInfo response) {
BodySubscriber subscriber = handler.apply(response);
- Http1ResponseBodySubscriber bs =
- new Http1ResponseBodySubscriber(subscriber, this);
- return bs;
+ var cancelTimerOnTermination =
+ cancelTimerOnResponseBodySubscriberTermination(
+ exchange.request().isWebSocket(), response.statusCode());
+ return new Http1ResponseBodySubscriber<>(subscriber, cancelTimerOnTermination, this);
}
@Override
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java
index 41a4a84958a..81475a47c4a 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java
@@ -554,8 +554,12 @@ final class Http3ExchangeImpl extends Http3Stream {
}
final class Http3StreamResponseSubscriber extends HttpBodySubscriberWrapper {
- Http3StreamResponseSubscriber(BodySubscriber subscriber) {
+
+ private final boolean cancelTimerOnTermination;
+
+ Http3StreamResponseSubscriber(BodySubscriber subscriber, boolean cancelTimerOnTermination) {
super(subscriber);
+ this.cancelTimerOnTermination = cancelTimerOnTermination;
}
@Override
@@ -568,6 +572,13 @@ final class Http3ExchangeImpl extends Http3Stream {
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 extends Http3Stream {
Http3StreamResponseSubscriber createResponseSubscriber(BodyHandler handler,
ResponseInfo response) {
if (debug.on()) debug.log("Creating body subscriber");
- Http3StreamResponseSubscriber subscriber =
- new Http3StreamResponseSubscriber<>(handler.apply(response));
- return subscriber;
+ var cancelTimerOnTermination =
+ cancelTimerOnResponseBodySubscriberTermination(
+ exchange.request().isWebSocket(), response.statusCode());
+ return new Http3StreamResponseSubscriber<>(handler.apply(response), cancelTimerOnTermination);
}
@Override
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
index d02930f4f31..ff130e90358 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
@@ -1880,6 +1880,13 @@ final class HttpClientImpl extends HttpClient implements Trackable {
}
}
+ // Visible for tests
+ List 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
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
index ec621f7f955..60eb55ec0ad 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
@@ -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 implements Cancelable {
.map(ConnectTimeoutTracker::getRemaining);
}
- private void cancelTimer() {
+ void cancelTimer() {
if (responseTimerEvent != null) {
client.cancelTimer(responseTimerEvent);
responseTimerEvent = null;
@@ -404,6 +403,8 @@ class MultiExchange implements Cancelable {
processAltSvcHeader(r, client(), currentreq);
Exchange 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 implements Cancelable {
private CompletableFuture 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 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 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";
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
index 9a7ccd8f3a1..bf9170f8f51 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
@@ -390,9 +390,10 @@ class Stream extends ExchangeImpl {
@Override
Http2StreamResponseSubscriber createResponseSubscriber(BodyHandler handler, ResponseInfo response) {
- Http2StreamResponseSubscriber 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 extends ExchangeImpl {
.whenComplete((v, t) -> pushGroup.pushError(t));
}
+ @Override
+ Http2StreamResponseSubscriber createResponseSubscriber(BodyHandler handler, ResponseInfo response) {
+ return new Http2StreamResponseSubscriber(handler.apply(response), false);
+ }
+
@Override
void completeResponse(Response r) {
Log.logResponse(r::toString);
@@ -1924,8 +1930,12 @@ class Stream extends ExchangeImpl {
}
final class Http2StreamResponseSubscriber extends HttpBodySubscriberWrapper {
- Http2StreamResponseSubscriber(BodySubscriber subscriber) {
+
+ private final boolean cancelTimerOnTermination;
+
+ Http2StreamResponseSubscriber(BodySubscriber subscriber, boolean cancelTimerOnTermination) {
super(subscriber);
+ this.cancelTimerOnTermination = cancelTimerOnTermination;
}
@Override
@@ -1938,6 +1948,13 @@ class Stream extends ExchangeImpl {
unregisterResponseSubscriber(this);
}
+ @Override
+ protected void onTermination() {
+ if (cancelTimerOnTermination) {
+ exchange.multi.cancelTimer();
+ }
+ }
+
}
private static final VarHandle DEREGISTERED;
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java
index 1c483ce99f4..f1c1f6f2d2a 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java
@@ -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 implements TrustedSubscriber {
public static final Comparator> 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 implements TrustedSubscriber {
this.userSubscriber = userSubscriber;
}
- private class SubscriptionWrapper implements Subscription {
+ /**
+ * A callback to be invoked before 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 implements TrustedSubscriber {
subscription.cancel();
} finally {
if (markCancelled()) {
+ onTermination();
onCancel();
}
}
@@ -284,6 +294,7 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber {
*/
public final void complete(Throwable t) {
if (markCompleted()) {
+ onTermination();
logComplete(t);
tryUnregister();
t = withError = Utils.getCompletionCause(t);
diff --git a/test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java b/test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java
new file mode 100644
index 00000000000..093885a6ba0
--- /dev/null
+++ b/test/jdk/java/net/httpclient/TimeoutResponseBodyTest.java
@@ -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 response body 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.
+ *
+ * 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 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 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);
+ }
+
+ }
+
+}
diff --git a/test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java b/test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java
new file mode 100644
index 00000000000..ab562f8eab8
--- /dev/null
+++ b/test/jdk/java/net/httpclient/TimeoutResponseHeaderTest.java
@@ -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 response header 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);
+ }
+ }
+
+}
diff --git a/test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java b/test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java
new file mode 100644
index 00000000000..4da63a2dff9
--- /dev/null
+++ b/test/jdk/java/net/httpclient/TimeoutResponseTestSupport.java
@@ -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 response body timeout tests
+ * @see TimeoutResponseHeaderTest Server response header timeout tests
+ * @see TimeoutBasic Server connection 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 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);
+ }
+
+}
diff --git a/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/HttpClientTimerAccess.java b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/HttpClientTimerAccess.java
new file mode 100644
index 00000000000..8bc1fac7b60
--- /dev/null
+++ b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/HttpClientTimerAccess.java
@@ -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 assertType(Class 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;
+ }
+
+}
diff --git a/test/jdk/java/net/httpclient/websocket/WebSocketTest.java b/test/jdk/java/net/httpclient/websocket/WebSocketTest.java
index 83f8b6eab27..43bcb054b7d 100644
--- a/test/jdk/java/net/httpclient/websocket/WebSocketTest.java
+++ b/test/jdk/java/net/httpclient/websocket/WebSocketTest.java
@@ -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()) {
From df0165bd6933728fdcf1956323401afdc47b3f78 Mon Sep 17 00:00:00 2001
From: Ana-Maria Mihalceanu
Date: Thu, 4 Dec 2025 10:09:33 +0000
Subject: [PATCH 009/413] 8321139: jlink's compression plugin doesn't handle -c
option correctly
Reviewed-by: jpai, alanb
---
.../jdk/tools/jlink/internal/TaskHelper.java | 7 ++-
.../tools/jlink/resources/plugins.properties | 12 ++---
src/jdk.jlink/share/man/jlink.md | 21 +++++---
test/jdk/tools/jlink/JLinkTest.java | 38 ++++++++++++--
test/jdk/tools/jlink/TaskHelperTest.java | 51 +++++++++++++++++--
test/setup_aot/TestSetupAOT.java | 2 +-
6 files changed, 106 insertions(+), 25 deletions(-)
diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java
index d53589dc388..dca4d57f764 100644
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java
@@ -362,10 +362,13 @@ public final class TaskHelper {
if (plugin instanceof DefaultCompressPlugin) {
plugOption
- = new PluginOption(false,
+ = new PluginOption(true,
(task, opt, arg) -> {
Map m = addArgumentMap(plugin);
- m.put(plugin.getName(), DefaultCompressPlugin.LEVEL_2);
+ String level = (arg != null && !arg.isEmpty())
+ ? arg
+ :"zip-6";
+ m.put(plugin.getName(), level);
}, false, "--compress", "-c");
mainOptions.add(plugOption);
} else if (plugin instanceof DefaultStripDebugPlugin) {
diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties
index e9be0b4e587..7e3c26fa7b8 100644
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties
@@ -61,14 +61,14 @@ Class optimization: convert Class.forName calls to constant loads.
class-for-name.usage=\
\ --class-for-name Class optimization: convert Class.forName calls to constant loads.
-compress.argument=[:filter=]
+compress.argument=[:filter=]
compress.description= Compression to use in compressing resources.
compress.usage=\
\ --compress Compression to use in compressing resources:\n\
\ Accepted values are:\n\
-\ zip-[0-9], where zip-0 provides no compression,\n\
+\ zip-'{0-9}', where zip-0 provides no compression,\n\
\ and zip-9 provides the best compression.\n\
\ Default is zip-6.
@@ -307,15 +307,15 @@ plugin.opt.disable-plugin=\
\ --disable-plugin Disable the plugin mentioned
plugin.opt.compress=\
-\ --compress Compression to use in compressing resources:\n\
+\ --compress Compress all resources in the output image:\n\
\ Accepted values are:\n\
-\ zip-[0-9], where zip-0 provides no compression,\n\
+\ zip-'{0-9}', where zip-0 provides no compression,\n\
\ and zip-9 provides the best compression.\n\
\ Default is zip-6.\n\
\ Deprecated values to be removed in a future release:\n\
-\ 0: No compression. Equivalent to zip-0.\n\
+\ 0: No compression. Use zip-0 instead.\n\
\ 1: Constant String Sharing\n\
-\ 2: Equivalent to zip-6.
+\ 2: ZIP. Use zip-6 instead.
plugin.opt.strip-debug=\
\ -G, --strip-debug Strip debug information
diff --git a/src/jdk.jlink/share/man/jlink.md b/src/jdk.jlink/share/man/jlink.md
index dc256af43b5..0d16e69c9ef 100644
--- a/src/jdk.jlink/share/man/jlink.md
+++ b/src/jdk.jlink/share/man/jlink.md
@@ -64,12 +64,15 @@ Developers are responsible for updating their custom runtime images.
`--bind-services`
: Link service provider modules and their dependencies.
-`-c ={0|1|2}` or `--compress={0|1|2}`
-: Enable compression of resources:
+`-c zip-{0-9}` or `--compress=zip-{0-9}`
+: Enable compression of resources. The accepted values are:
+ zip-{0-9}, where zip-0 provides no compression,
+ and zip-9 provides the best compression. Default is zip-6.
- - `0`: No compression
+: Deprecated values to be removed in a future release:
+ - `0`: No compression. Use zip-0 instead.
- `1`: Constant string sharing
- - `2`: ZIP
+ - `2`: ZIP. Use zip-6 instead.
`--disable-plugin` *pluginname*
: Disables the specified plug-in. See [jlink Plug-ins] for the list of
@@ -170,14 +173,18 @@ For a complete list of all available plug-ins, run the command
### Plugin `compress`
Options
-: `--compress=`{`0`\|`1`\|`2`}\[`:filter=`*pattern-list*\]
+: `--compress=zip-`{`0`-`9`}\[`:filter=`*pattern-list*\]
Description
: Compresses all resources in the output image.
+ Accepted values are:
+ zip-{0-9}, where zip-0 provides no compression,
+ and zip-9 provides the best compression. Default is zip-6.
- - Level 0: No compression
+: Deprecated values to be removed in a future release:
+ - Level 0: No compression. Use zip-0 instead.
- Level 1: Constant string sharing
- - Level 2: ZIP
+ - Level 2: ZIP. Use zip-6 instead.
An optional *pattern-list* filter can be specified to list the pattern of
files to include.
diff --git a/test/jdk/tools/jlink/JLinkTest.java b/test/jdk/tools/jlink/JLinkTest.java
index bc4d2a08800..1d84caaa147 100644
--- a/test/jdk/tools/jlink/JLinkTest.java
+++ b/test/jdk/tools/jlink/JLinkTest.java
@@ -42,10 +42,7 @@ import tests.JImageGenerator;
/*
* @test
* @summary Test image creation
- * @bug 8189777
- * @bug 8194922
- * @bug 8206962
- * @bug 8240349
+ * @bug 8189777 8194922 8206962 8240349 8163382 8165735 8166810 8173717 8321139
* @author Jean-Francois Denise
* @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g)
* @library ../lib
@@ -358,6 +355,39 @@ public class JLinkTest {
helper.generateDefaultImage(userOptions, moduleName).assertFailure("Error: Invalid compression level invalid");
}
+ // short command without argument
+ {
+ String[] userOptions = {"-c"};
+ String moduleName = "invalidCompressLevelEmpty";
+ helper.generateDefaultJModule(moduleName, "composite2");
+ helper.generateDefaultImage(userOptions, moduleName).assertFailure("Error: no value given for -c");
+ }
+
+ // invalid short command
+ {
+ String[] userOptions = {"-c", "3", "--output", "image"};
+ String moduleName = "invalidCompressLevel3";
+ helper.generateDefaultJModule(moduleName, "composite2");
+ helper.generateDefaultImage(userOptions, moduleName).assertFailure("Error: Invalid compression level 3");
+ }
+
+
+ // invalid argument value
+ {
+ String[] userOptions = {"--compress", "42", "--output", "image"};
+ String moduleName = "invalidCompressLevel42";
+ helper.generateDefaultJModule(moduleName, "composite2");
+ helper.generateDefaultImage(userOptions, moduleName).assertFailure("Error: Invalid compression level 42");
+ }
+
+ // invalid argument value
+ {
+ String[] userOptions = {"--compress", "zip-", "--output", "image"};
+ String moduleName = "invalidCompressLevelZip";
+ helper.generateDefaultJModule(moduleName, "composite2");
+ helper.generateDefaultImage(userOptions, moduleName).assertFailure("Error: Invalid compression level zip-");
+ }
+
// orphan argument - JDK-8166810
{
String[] userOptions = {"--compress", "2", "foo" };
diff --git a/test/jdk/tools/jlink/TaskHelperTest.java b/test/jdk/tools/jlink/TaskHelperTest.java
index 51dea8de24a..26ac376a6ec 100644
--- a/test/jdk/tools/jlink/TaskHelperTest.java
+++ b/test/jdk/tools/jlink/TaskHelperTest.java
@@ -48,7 +48,7 @@ import jdk.tools.jlink.internal.TaskHelper.BadArgs;
/*
* @test
* @summary Test TaskHelper option parsing
- * @bug 8303884
+ * @bug 8303884 8321139
* @modules jdk.jlink/jdk.tools.jlink.internal
* jdk.jlink/jdk.tools.jlink.plugin
* @run junit TaskHelperTest
@@ -59,19 +59,22 @@ public class TaskHelperTest {
private static final List