B.28. HTTP Timeout Handler

Pada chapter B.22. Simple Configuration kita telah mempelajari cara penggunaan ReadTimeout dan WriteTimeout pada http.Server. Timeout tersebut bersifat global untuk semua endpoint. Go juga menyediakan http.TimeoutHandler yang memungkinkan kita menentukan timeout per handler, sehingga endpoint yang lambat tidak mempengaruhi endpoint lain.

Pada chapter ini kita akan belajar timeout spesifik per handler.

B.28.1. Perbedaan Server Timeout dan http.TimeoutHandler

Server Timeout (ReadTimeout/WriteTimeout) http.TimeoutHandler
Scope Seluruh server Per handler
Aksi saat timeout Koneksi TCP diputus paksa Response 503 Service Unavailable dikirim
Goroutine handler Tetap berjalan Tetap berjalan (tidak di-cancel)

Poin penting: http.TimeoutHandler tidak membatalkan goroutine handler yang sedang berjalan. Handler tetap berjalan di background, hanya response ke client yang dipotong. Untuk membatalkan proses di handler, perlu dikombinasikan dengan context dari r.Context().Done() (lihat chapter B.30. Server Handler HTTP Request Cancellation).

B.28.2. Penggunaan http.TimeoutHandler

Cara menggunakan http.TimeoutHandler() adalah dengan menjadikannya sebagai handler func untuk membungkus handler sebenarnya. Fungsi ini skema-nya seperti ini:

http.TimeoutHandler(handler, timeout, message)

Penjelasan parameter:

  1. handler: handler yang dibungkus, bertipe http.Handler
  2. timeout: durasi timeout bertipe time.Duration
  3. message: pesan yang dikirim ke client saat timeout

B.28.3. Implementasi

Mari coba praktikkan, buat file main.go dengan dua handler: satu cepat dan satu lambat, keduanya dibungkus http.TimeoutHandler dengan durasi 3 detik.

package main

import (
    "log"
    "net/http"
    "time"
)

func handleFast(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("response cepat"))
}

func handleSlow(w http.ResponseWriter, r *http.Request) {
    time.Sleep(5 * time.Second)
    w.Write([]byte("response lambat selesai"))
}

func main() {
    mux := http.NewServeMux()

    mux.Handle("GET /fast", http.TimeoutHandler(
        http.HandlerFunc(handleFast),
        3*time.Second,
        "request timeout",
    ))

    mux.Handle("GET /slow", http.TimeoutHandler(
        http.HandlerFunc(handleSlow),
        3*time.Second,
        "request timeout: proses terlalu lama",
    ))

    log.Println("server started at localhost:9000")
    err := http.ListenAndServe(":9000", mux)
    if err != nil {
        log.Fatal(err)
    }
}

handleFast() langsung menulis response tanpa jeda, sehingga selesai jauh sebelum batas 3 detik. handleSlow() sebaliknya: ada time.Sleep(5 * time.Second) yang membuatnya selalu melewati timeout.

Masing-masing handler dibungkus dengan http.TimeoutHandler saat didaftarkan ke mux. http.HandlerFunc(handleFast) digunakan untuk mengonversi fungsi biasa ke tipe http.Handler yang dibutuhkan sebagai parameter pertama http.TimeoutHandler. Parameter ketiga adalah pesan yang dikirim ke client saat timeout terjadi.

B.28.4. Testing

Jalankan server lalu coba dua skenario berikut.

curl http://localhost:9000/fast

Response: response cepat dengan status 200.

curl -i http://localhost:9000/slow

Response: HTTP/1.1 503 Service Unavailable dengan body request timeout: proses terlalu lama setelah menunggu 3 detik.

B.28.5. Kombinasi dengan Context Cancellation

Untuk benar-benar membatalkan proses di dalam handler saat timeout, pantau r.Context().Done() di dalam handler.

func handleSlow(w http.ResponseWriter, r *http.Request) {
    select {
    case <-time.After(5 * time.Second):
        w.Write([]byte("selesai"))
    case <-r.Context().Done():
        log.Println("handler dibatalkan karena timeout")
        return
    }
}

Dengan pola ini, ketika http.TimeoutHandler meng-cancel context setelah timeout, handler juga ikut berhenti sehingga tidak membuang resource.