Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8345105

HttpRequest.timeout() and HttpClient.connectTimeout() throw different exceptions for address resolution failure

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: P4 P4
    • tbd
    • 21.0.5
    • core-libs
    • x86_64
    • linux_oracle_6.0

      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


            jpai Jaikiran Pai
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: