นาฬิกาและแผงคำขอ HTTP บนโต๊ะมิ้นต์มีเข็มชี้ใกล้หมดเวลาและนากกายยืนมุมซ้าย

Java HttpClient: ตั้ง request timeout ไม่ให้ค้างรอปลายทาง

น้องกายEngineering

ผมเคยเขียน HTTP เรียกแล้วเธรดค้างเพราะปลายทางไม่ตอบ จนรู้ว่า HttpClient แยก timeout ของ connect กับของแต่ละคำขอได้ครับ

เอกสาร HttpClient และ HttpRequest.Builder#timeout อธิบายว่าตั้ง Duration ต่อคำขอได้ เมื่อหมดเวลาก่อนได้ผลลัพธ์ จะได้ HttpTimeoutException

ทำไมควรตั้งต่อคำขอ

timeout ทั้ง client ช่วยตอนเริ่มต่อ แต่คำขอที่อ่าน body นานยังค้างได้ การใส่ timeout ใน HttpRequest ทำให้แต่ละเส้นทางมีเพดานชัด เช่น health check สั้นกว่าดาวน์โหลดรายงาน

ตัวอย่างด้านล่างยืนยันว่า timeout ถูกแนบกับคำขอจริง และเมื่อส่งไปยังพอร์ตที่ไม่มีบริการ เครื่องจะล้มเร็วโดยไม่ค้างยาว

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

public class Main {
    public static void main(String[] args) throws Exception {
        Duration requestTimeout = Duration.ofMillis(250);
        HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:1/health"))
            .timeout(requestTimeout)
            .GET()
            .build();

        assert request.timeout().isPresent();
        assert request.timeout().get().equals(requestTimeout);

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofMillis(200))
            .build();

        long started = System.nanoTime();
        boolean failedFast = false;
        try {
            client.send(request, HttpResponse.BodyHandlers.discarding());
        } catch (IOException expected) {
            failedFast = true;
        }
        long elapsedMs = Duration.ofNanos(System.nanoTime() - started).toMillis();

        assert failedFast : "expected connection failure against a closed local port";
        assert elapsedMs < 3000 : "client stayed blocked too long: " + elapsedMs + "ms";
        System.out.println("ok");
    }
}

ถ้าต้องการยกเลิกกลางคันเพิ่มเติม ใช้ sendAsync คู่กับ CompletableFuture และยกเลิกเอง แต่ขั้นแรกที่ขาดบ่อยคือใส่ timeout ใน request ให้ครบทุก call site ที่รอผล

Recap

  • ตั้ง HttpRequest timeout แยกจาก connect timeout
  • ตรวจในเทสว่า timeout ถูกแนบกับคำขอ และความล้มเหลวเร็วไม่ค้าง
  • จับ HttpTimeoutException ในเส้นทางที่ปลายทางช้าจริง
  • ไล่ call site ที่ยังไม่มี timeout ก่อนปล่อยโปรดักชัน