Help us understand the problem. What is going on with this article?

Java HTTP Client API のタイムアウト設定

概要

  • Java 11 から正式導入された HTTP Client API (java.net.http パッケージ) のタイムアウト設定について接続時の動作を検証する
  • HTTP Client API では設定できるタイムアウト値が2種類ある
    • 接続タイムアウト: HttpClient.Builder.connectTimeout
    • リクエストタイムアウト: HttpRequest.Builder.timeout

検証結果からの考察

  • 接続タイムアウト HttpClient.Builder.connectTimeout には接続タイムアウトの許容時間を指定する
  • リクエストタイムアウト HttpRequest.Builder.timeout にはリクエスト全体 (接続タイムアウトや読み取りタイムアウトを含めたもの) の許容時間を指定する

公式ドキュメントによる情報

HttpClient.Builder.connectTimeout

HttpClient.Builder (Java SE 14 & JDK 14)

HttpClient.Builder connectTimeout​(Duration duration)

このクライアントの接続タイムアウト期間を設定します。
新しい接続を確立する必要がある場合に、指定された duration内に接続を確立できないと、HttpClient::sendはHttpConnectTimeoutExceptionをスローし、HttpClient::sendAsyncはHttpConnectTimeoutExceptionを使用して例外的に完了します。 たとえば、接続を前のリクエストから再利用できる場合など、新しい接続を確立する必要がない場合、このタイムアウト期間は影響しません。

パラメータ:
duration - 基礎となる接続の確立を許可する期間

戻り値:
このビルダー

例外:
IllegalArgumentException - 期間が非正の場合

HttpClient.Builder (Java SE 14 & JDK 14)

HttpClient.Builder connectTimeout​(Duration duration)

Sets the connect timeout duration for this client.
In the case where a new connection needs to be established, if the connection cannot be established within the given duration, then HttpClient::send throws an HttpConnectTimeoutException, or HttpClient::sendAsync completes exceptionally with an HttpConnectTimeoutException. If a new connection does not need to be established, for example if a connection can be reused from a previous request, then this timeout duration has no effect.

Parameters:
duration - the duration to allow the underlying connection to be established

Returns:
this builder

Throws:
IllegalArgumentException - if the duration is non-positive

HttpRequest.Builder.timeout

HttpRequest.Builder (Java SE 14 & JDK 14)

HttpRequest.Builder timeout​(Duration duration)

このリクエストのタイムアウトを設定します。 指定されたタイムアウト内にレスポンスを受信しなかった場合、HttpClient::sendまたはHttpClient::sendAsyncからHttpTimeoutExceptionが例外的にHttpTimeoutExceptionを使ってスローされます。 タイムアウトを設定しない場合の影響は、無限期間(永続的ブロック)の設定と同じです。

パラメータ:
duration - タイムアウト期間

戻り値:
このビルダー

例外:
IllegalArgumentException - 期間が非正の場合

HttpRequest.Builder (Java SE 14 & JDK 14)

HttpRequest.Builder timeout​(Duration duration)

Sets a timeout for this request. If the response is not received within the specified timeout then an HttpTimeoutException is thrown from HttpClient::send or HttpClient::sendAsync completes exceptionally with an HttpTimeoutException. The effect of not setting a timeout is the same as setting an infinite Duration, i.e. block forever.

Parameters:
duration - the timeout duration

Returns:
this builder

Throws:
IllegalArgumentException - if the duration is non-positive

挙動を検証するソースコード

HttpClientTimeoutTest.java というファイル名で以下の内容を保存する。

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.LocalDateTime;

public class HttpClientTimeoutTest {

  public static void main(String[] args) throws Exception {

    // 接続タイムアウトのテスト (存在しないサーバ、接続タイムアウト3秒)
    testTimeout("http://127.0.0.2:8000/", Duration.ofSeconds(3), null);
    Thread.sleep(1000L);

    // 読み取りタイムアウトのテスト (レスポンスが遅いサーバ、リクエストタイムアウト3秒)
    testTimeout("http://127.0.0.1:8000/", null, Duration.ofSeconds(3));
    Thread.sleep(1000L);

    // リクエストタイムアウトの影響範囲のテスト (存在しないサーバ、接続タイムアウト5秒、リクエストタイムアウト3秒)
    testTimeout("http://127.0.0.2:8000/", Duration.ofSeconds(5), Duration.ofSeconds(3));
    Thread.sleep(1000L);
  }

  /**
   * タイムアウトのテスト。
   * @param url URL
   * @param connectTimeout HttpClient.Builder.connectTimeout にセットする値
   * @param requestTimeout HttpRequest.Builder.timeout にセットする値
   */
  private static void testTimeout(String url, Duration connectTimeout, Duration requestTimeout) {

    try {
      // パラメータを確認
      System.out.println("URL: " + url);
      System.out.println("ConnectTimeout: " + connectTimeout);
      System.out.println("RequestTimeout: " + requestTimeout);

      // HttpClient オブジェクトを構築
      HttpClient.Builder hcb = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1);
      if (connectTimeout != null) {
        hcb.connectTimeout(connectTimeout); // 接続タイムアウトを設定
      }
      HttpClient client = hcb.build();

      // HttpRequest オブジェクトを構築
      HttpRequest.Builder hrb = HttpRequest.newBuilder(new URI(url)).GET();
      if (requestTimeout != null) {
        hrb.timeout(requestTimeout); // リクエストタイムアウトを設定
      }
      HttpRequest request = hrb.build();

      // タイムアウト設定を確認
      System.out.println("HttpClient.connectTimeout: " + client.connectTimeout());
      System.out.println("HttpRequest.timeout: " + request.timeout());

      // HTTP リクエストを投げる
      System.out.println("HttpClient.send begins: " + LocalDateTime.now());
      HttpResponse<String> res = client.send(request, HttpResponse.BodyHandlers.ofString());

    } catch (Exception e) {
      System.out.println("HttpClient.send failed: " + LocalDateTime.now());
      e.printStackTrace();
    }
  }
}

レスポンスが遅い HTTP サーバを用意

リクエストが来てから10秒後にレスポンスを返すような HTTP サーバを Ruby で書く。

server.rb というファイル名で以下の内容を保存する。

require 'webrick'

server = WEBrick::HTTPServer.new({
  :Port => 8000,
  :HTTPVersion => WEBrick::HTTPVersion.new('1.1')
})

server.mount_proc('/') do |req, res|
  sleep(10) # 10秒スリープ
  res.status = 200
  res.body = ''
end

Signal.trap('INT'){server.shutdown}
server.start

ruby コマンドで HTTP サーバのプログラムを実行する。

$ ruby server.rb
[2020-06-28 11:03:15] INFO  WEBrick 1.6.0
[2020-06-28 11:03:15] INFO  ruby 2.7.1 (2020-03-31) [x86_64-darwin19]
[2020-06-28 11:03:15] INFO  WEBrick::HTTPServer#start: pid=47723 port=8000

実行結果

java コマンドで HttpClientTimeoutTest.java を実行する。

Java 11 移行であれば javac によるコンパイル無しで実行できる。
参考: JEP 330: Launch Single-File Source-Code Programs

今回の検証環境: macOS Catalina + Java 14 (AdoptOpenJDK 14.0.1+7)

$ java HttpClientTimeoutTest.java
URL: http://127.0.0.2:8000/
ConnectTimeout: PT3S
RequestTimeout: null
HttpClient.connectTimeout: Optional[PT3S]
HttpRequest.timeout: Optional.empty
HttpClient.send begins: 2020-06-28T11:07:13.619689
HttpClient.send failed: 2020-06-28T11:07:16.703287
java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:557)
    at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
    at HttpClientTimeoutTest.testTimeout(HttpClientTimeoutTest.java:59)
    at HttpClientTimeoutTest.main(HttpClientTimeoutTest.java:13)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:415)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
Caused by: java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.MultiExchange.toTimeoutException(MultiExchange.java:508)
    at java.net.http/jdk.internal.net.http.MultiExchange.getExceptionalCF(MultiExchange.java:455)
    at java.net.http/jdk.internal.net.http.MultiExchange.lambda$responseAsyncImpl$7(MultiExchange.java:382)
    at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:930)
    at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:907)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2152)
    at java.net.http/jdk.internal.net.http.Http1Exchange.lambda$cancelImpl$9(Http1Exchange.java:482)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
    at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: java.net.ConnectException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.MultiExchange.toTimeoutException(MultiExchange.java:509)
    ... 10 more
URL: http://127.0.0.1:8000/
ConnectTimeout: null
RequestTimeout: PT3S
HttpClient.connectTimeout: Optional.empty
HttpRequest.timeout: Optional[PT3S]
HttpClient.send begins: 2020-06-28T11:07:17.713737
HttpClient.send failed: 2020-06-28T11:07:20.720473
java.net.http.HttpTimeoutException: request timed out
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:561)
    at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
    at HttpClientTimeoutTest.testTimeout(HttpClientTimeoutTest.java:59)
    at HttpClientTimeoutTest.main(HttpClientTimeoutTest.java:17)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:415)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
URL: http://127.0.0.2:8000/
ConnectTimeout: PT5S
RequestTimeout: PT3S
HttpClient.connectTimeout: Optional[PT5S]
HttpRequest.timeout: Optional[PT3S]
HttpClient.send begins: 2020-06-28T11:07:21.729119
HttpClient.send failed: 2020-06-28T11:07:24.735766
java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:557)
    at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
    at HttpClientTimeoutTest.testTimeout(HttpClientTimeoutTest.java:59)
    at HttpClientTimeoutTest.main(HttpClientTimeoutTest.java:21)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:415)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
Caused by: java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.ResponseTimerEvent.handle(ResponseTimerEvent.java:68)
    at java.net.http/jdk.internal.net.http.HttpClientImpl.purgeTimeoutsAndReturnNextDeadline(HttpClientImpl.java:1259)
    at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:888)
Caused by: java.net.ConnectException: HTTP connect timed out
    at java.net.http/jdk.internal.net.http.ResponseTimerEvent.handle(ResponseTimerEvent.java:69)
    ... 2 more

実行結果の検証

接続タイムアウトのテスト (存在しないサーバ、接続タイムアウト3秒)

  • 接続タイムアウト: HttpClient.Builder.connectTimeout に3秒を指定
  • リクエストタイムアウト: HttpRequest.Builder.timeout は無指定
  • 3秒で接続タイムアウトが発生
  • 例外 java.net.http.HttpConnectTimeoutException が発生

  • 例外チェーン

    • java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    • Caused by: java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    • Caused by: java.net.ConnectException: HTTP connect timed out

読み取りタイムアウトのテスト (レスポンスが遅いサーバ、リクエストタイムアウト3秒)

  • 接続タイムアウト: HttpClient.Builder.connectTimeout は無指定
  • リクエストタイムアウト: HttpRequest.Builder.timeout に3秒を指定
  • 3秒でリクエストタイムアウトが発生
  • 例外 java.net.http.HttpTimeoutException が発生

  • 例外チェーン (原因例外は無し)

    • java.net.http.HttpTimeoutException: request timed out

リクエストタイムアウトの影響範囲のテスト (存在しないサーバ、接続タイムアウト5秒、リクエストタイムアウト3秒)

  • 接続タイムアウト: HttpClient.Builder.connectTimeout に5秒を指定
  • リクエストタイムアウト: HttpRequest.Builder.timeout に3秒を指定
  • 3秒で接続タイムアウトが発生
    • 接続タイムアウトに5秒を指定しているのにもかかわらず3秒で接続タイムアウトが発生している
    • リクエストタイムアウトに設定した3秒で接続タイムアウトが発生していると思われる
    • リクエストタイムアウトは接続タイムアウトの時間込みの値だと考えられる
  • 例外 java.net.http.HttpTimeoutException が発生

  • 例外チェーン

    • java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    • Caused by: java.net.http.HttpConnectTimeoutException: HTTP connect timed out
    • Caused by: java.net.ConnectException: HTTP connect timed out

参考資料

niwasawa
迷子になりがちな地図・位置情報系プログラマ。
http://niwasawa.hatenablog.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした