アイキャッチ

実装・エラー解決

JavaのHttpUrlConnectionのリクエストタイムアウトとレスポンスタイムアウトの違い

このページに書いてあること。JavaのHttpURLConnectionで、接続待ちと読み取り待ちを切り分け、読み取りタイムアウトの後に再試行する方法を説明します。

背景。接続に使う setConnectTimeout と、応答を読む待ち時間に使う setReadTimeout は別です。HTTP応答のヘッダーが届く前の待ち時間は、getResponseCode() の中で読み取りタイムアウトとして現れることがあります。

発生した問題。読み取りタイムアウトを受けた後、同じHttpURLConnectionに再度 getResponseCode() を呼ぶと、手元のJDKでは再接続したときのように待たず、再び例外になりました。JavaのAPIでは、HttpURLConnectionインスタンスは1リクエスト用です。

確認コード。最初だけ応答を遅らせるローカルサーバーを同じプロセスで起動します。最初の読み取り、同じインスタンスでの呼び直し、新規インスタンスでのGETを比べます。

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ReadTimeoutExample {
    public static void main(String[] args) throws IOException {
        var executor = Executors.newCachedThreadPool();
        var requests = new AtomicInteger();
        var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        server.setExecutor(executor);
        server.createContext("/", exchange -> {
            try {
                if (requests.incrementAndGet() == 1) {
                    Thread.sleep(750);
                }
                exchange.sendResponseHeaders(200, -1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                exchange.close();
            }
        });
        server.start();

        URL url = new URL("https://127.0.0.1:" + server.getAddress().getPort() + "/");
        HttpURLConnection first = open(url);
        try {
            try {
                first.getResponseCode();
                throw new AssertionError("expected a read timeout");
            } catch (SocketTimeoutException expected) {
                System.out.println("first request -> " + expected.getClass().getSimpleName());
            }

            try {
                System.out.println("same instance -> HTTP " + first.getResponseCode());
            } catch (IOException observed) {
                System.out.println("same instance -> " + observed.getClass().getSimpleName());
            }

            HttpURLConnection retry = open(url);
            try {
                int status = retry.getResponseCode();
                if (status != 200) {
                    throw new AssertionError("expected HTTP 200, got " + status);
                }
                System.out.println("new instance -> HTTP " + status);
            } finally {
                retry.disconnect();
            }
        } finally {
            first.disconnect();
            server.stop(0);
            executor.shutdownNow();
        }
    }

    private static HttpURLConnection open(URL url) throws IOException {
        var connection = (HttpURLConnection) url.openConnection();
        connection.setConnectTimeout(1000);
        connection.setReadTimeout(250);
        return connection;
    }
}
javac ReadTimeoutExample.java
java ReadTimeoutExample
# first request -> SocketTimeoutException
# same instance -> SocketTimeoutException
# new instance -> HTTP 200

出力例はJDK 17.0.8の観測値です。同じインスタンスで例外になる具体的な経路は実装に依存します。再試行する場合は、新規インスタンスでリクエストを作り、今回のようなGETと、処理済みか不明な更新系リクエストとで扱いを分けます。

出典 Java SE 17のHttpURLConnection

-実装・エラー解決
-,