このページに書いてあること。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と、処理済みか不明な更新系リクエストとで扱いを分けます。
