ADDITIONAL SYSTEM INFORMATION :
==> /etc/oracle-release <==
Oracle Linux Server release 9.5
==> /etc/os-release <==
NAME="Oracle Linux Server"
==> /etc/redhat-release <==
Red Hat Enterprise Linux release 9.5 (Plow)
==> /etc/system-release <==
Oracle Linux Server release 9.5
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment (Red_Hat-21.0.5.0.11-1.0.1) (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-21.0.5.0.11-1.0.1) (build 21.0.5+11-LTS, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
When HttpRequest.timeout() is used for an HttpClient request (synchronous or asynchronous) and the HttpRequest.timeout() is less than the DNS resolution timeout (defaults to 10 seconds on Linux) and a DNS resolution failure happens, a "ConnectException: HTTP connect timed out" exception is thrown instead of an UnresolvedAddressException.
The same problem does not happen when HttpClient.connectTimeout() is used or if the HttpRequest.timeout() is longer than the system DNS resolution timeout.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Two options:
Option 1:
git clone https://github.com/deejgregor/httpclient-dns-failure.git
cd httpclient-dns-failure
./test.sh no-dns-faster-timeout-connect
Also, run "./test.sh" to see a group of test cases run, including cases with the patch provided below.
Option 2:
Break DNS resolution on your system so it points to a non-existent resolver (by putting something like this in /etc/resolv.conf on Linux, for example):
nameserver 192.0.2.1
options timeout:2
Then run:
# this is the same as the file included below
curl -fO https://raw.githubusercontent.com/deejgregor/httpclient-dns-failure/refs/heads/main/HttpClientTimeout.java
java HttpClientTimeout.java sync https://www.google.com/ PT3S -
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
...
no-dns-faster-timeout-connect: FAILURE! Took 4.118 seconds: Root cause exception: java.nio.channels.UnresolvedAddressException
Note the UnresolvedAddressException.
ACTUAL -
...
no-dns-faster-timeout-connect: FAILURE! Took 4.118 seconds: Root cause exception: java.net.ConnectException: HTTP connect timed out
Note the ConnectException.
---------- BEGIN SOURCE ----------
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.*;
import java.time.Duration;
import static java.time.temporal.ChronoUnit.SECONDS;
public class Main {
public static void main(String[] argv) throws URISyntaxException {
if (argv.length != 4) {
System.err.println("Incorrect number of command-line arguments.");
System.err.println("usage: java HttpClientTimeout.java sync|async <url> <request timeout> <connect timeout>");
System.err.println("<request timeout> and <connect timeout> should be ISO-8601 durations (e.g.: 'PT2S') or '-'");
System.err.println("See https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)");
System.exit(1);
}
String mode = argv[0];
String url = argv[1];
String requestTimeout = argv[2];
String connectTimeout = argv[3];
boolean async = false;
if ("sync".equals(mode)) {
async = false;
} else if ("async".equals(mode)) {
async = true;
} else {
System.err.println("first argument must be either 'sync' or 'async', not '" + mode + "'");
System.exit(1);
}
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(new URI(url))
.GET();
if (!"-".equals(requestTimeout)) {
requestBuilder.timeout(Duration.parse(requestTimeout));
}
HttpRequest request = requestBuilder.build();
HttpResponse response = null;
long start = System.currentTimeMillis();
Throwable e = null;
Throwable rootCause = null;
try {
HttpClient.Builder clientBuilder = HttpClient.newBuilder();
if (!"-".equals(connectTimeout)) {
clientBuilder.connectTimeout(Duration.parse(connectTimeout));
}
HttpClient client = clientBuilder.build();
if (async) {
response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).get();
} else {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
}
} catch (Throwable t) {
e = t;
rootCause = e;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
}
long end = System.currentTimeMillis();
if (rootCause == null) {
System.out.println(String.format("SUCCESS! Took %.03f seconds: %s", ((end - start) / 1000.0), response));
} else {
System.out.println(String.format("FAILURE! Took %.03f seconds: Root cause exception: %s", ((end - start) / 1000.0), rootCause));
if ("true".equals(System.getenv("DEBUG"))) {
e.printStackTrace();
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Use HttpClient.connectTimeout() instead. Or set the HttpRequest.timeout() to be longer than the DNS timeout (or shorten the DNS timeout). Or apply this patch:
https://github.com/deejgregor/httpclient-dns-failure/blob/main/multiexchange.patch
diff --git src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
index afa71130ef9..b3487564886 100644
--- src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
+++ src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
@@ -561,8 +561,8 @@ class MultiExchange<T> implements Cancelable {
return failedFuture(t);
}
- private HttpTimeoutException toTimeoutException(IOException ioe) {
- HttpTimeoutException t = null;
+ private IOException toTimeoutException(IOException ioe) {
+ IOException t = null;
// more specific, "request timed out", when connected
Exchange<?> exchange = getExchange();
@@ -576,8 +576,7 @@ class MultiExchange<T> implements Cancelable {
}
}
if (t == null) {
- t = new HttpConnectTimeoutException("HTTP connect timed out");
- t.initCause(new ConnectException("HTTP connect timed out"));
+ t = ioe;
}
return t;
}
FREQUENCY : always
==> /etc/oracle-release <==
Oracle Linux Server release 9.5
==> /etc/os-release <==
NAME="Oracle Linux Server"
==> /etc/redhat-release <==
Red Hat Enterprise Linux release 9.5 (Plow)
==> /etc/system-release <==
Oracle Linux Server release 9.5
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment (Red_Hat-21.0.5.0.11-1.0.1) (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-21.0.5.0.11-1.0.1) (build 21.0.5+11-LTS, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
When HttpRequest.timeout() is used for an HttpClient request (synchronous or asynchronous) and the HttpRequest.timeout() is less than the DNS resolution timeout (defaults to 10 seconds on Linux) and a DNS resolution failure happens, a "ConnectException: HTTP connect timed out" exception is thrown instead of an UnresolvedAddressException.
The same problem does not happen when HttpClient.connectTimeout() is used or if the HttpRequest.timeout() is longer than the system DNS resolution timeout.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Two options:
Option 1:
git clone https://github.com/deejgregor/httpclient-dns-failure.git
cd httpclient-dns-failure
./test.sh no-dns-faster-timeout-connect
Also, run "./test.sh" to see a group of test cases run, including cases with the patch provided below.
Option 2:
Break DNS resolution on your system so it points to a non-existent resolver (by putting something like this in /etc/resolv.conf on Linux, for example):
nameserver 192.0.2.1
options timeout:2
Then run:
# this is the same as the file included below
curl -fO https://raw.githubusercontent.com/deejgregor/httpclient-dns-failure/refs/heads/main/HttpClientTimeout.java
java HttpClientTimeout.java sync https://www.google.com/ PT3S -
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
...
no-dns-faster-timeout-connect: FAILURE! Took 4.118 seconds: Root cause exception: java.nio.channels.UnresolvedAddressException
Note the UnresolvedAddressException.
ACTUAL -
...
no-dns-faster-timeout-connect: FAILURE! Took 4.118 seconds: Root cause exception: java.net.ConnectException: HTTP connect timed out
Note the ConnectException.
---------- BEGIN SOURCE ----------
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.*;
import java.time.Duration;
import static java.time.temporal.ChronoUnit.SECONDS;
public class Main {
public static void main(String[] argv) throws URISyntaxException {
if (argv.length != 4) {
System.err.println("Incorrect number of command-line arguments.");
System.err.println("usage: java HttpClientTimeout.java sync|async <url> <request timeout> <connect timeout>");
System.err.println("<request timeout> and <connect timeout> should be ISO-8601 durations (e.g.: 'PT2S') or '-'");
System.err.println("See https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)");
System.exit(1);
}
String mode = argv[0];
String url = argv[1];
String requestTimeout = argv[2];
String connectTimeout = argv[3];
boolean async = false;
if ("sync".equals(mode)) {
async = false;
} else if ("async".equals(mode)) {
async = true;
} else {
System.err.println("first argument must be either 'sync' or 'async', not '" + mode + "'");
System.exit(1);
}
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(new URI(url))
.GET();
if (!"-".equals(requestTimeout)) {
requestBuilder.timeout(Duration.parse(requestTimeout));
}
HttpRequest request = requestBuilder.build();
HttpResponse response = null;
long start = System.currentTimeMillis();
Throwable e = null;
Throwable rootCause = null;
try {
HttpClient.Builder clientBuilder = HttpClient.newBuilder();
if (!"-".equals(connectTimeout)) {
clientBuilder.connectTimeout(Duration.parse(connectTimeout));
}
HttpClient client = clientBuilder.build();
if (async) {
response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).get();
} else {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
}
} catch (Throwable t) {
e = t;
rootCause = e;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
}
long end = System.currentTimeMillis();
if (rootCause == null) {
System.out.println(String.format("SUCCESS! Took %.03f seconds: %s", ((end - start) / 1000.0), response));
} else {
System.out.println(String.format("FAILURE! Took %.03f seconds: Root cause exception: %s", ((end - start) / 1000.0), rootCause));
if ("true".equals(System.getenv("DEBUG"))) {
e.printStackTrace();
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Use HttpClient.connectTimeout() instead. Or set the HttpRequest.timeout() to be longer than the DNS timeout (or shorten the DNS timeout). Or apply this patch:
https://github.com/deejgregor/httpclient-dns-failure/blob/main/multiexchange.patch
diff --git src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
index afa71130ef9..b3487564886 100644
--- src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
+++ src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
@@ -561,8 +561,8 @@ class MultiExchange<T> implements Cancelable {
return failedFuture(t);
}
- private HttpTimeoutException toTimeoutException(IOException ioe) {
- HttpTimeoutException t = null;
+ private IOException toTimeoutException(IOException ioe) {
+ IOException t = null;
// more specific, "request timed out", when connected
Exchange<?> exchange = getExchange();
@@ -576,8 +576,7 @@ class MultiExchange<T> implements Cancelable {
}
}
if (t == null) {
- t = new HttpConnectTimeoutException("HTTP connect timed out");
- t.initCause(new ConnectException("HTTP connect timed out"));
+ t = ioe;
}
return t;
}
FREQUENCY : always