Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

TimeoutHandlerでnet/httpのリクエストをタイムアウトさせる

この記事はQiitaで公開されていました

素直な実装

特に何もしない場合、net/httpタイムアウトしません。以下のサーバにHTTPアクセスをすると、1分後にhello!というレスポンスを返します。

package main

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

type handler struct{}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    <-time.After(1 * time.Minute)
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello!")
    log.Println("ok")
}

func main() {
    log.SetFlags(0)
    log.SetPrefix("server: ")

    var h handler
    http.Handle("/", &h)
    http.ListenAndServe(":8080", nil)
}

以下は手元での実行例です。

$ go run server.go &
$ curl http://localhost:8080/
server: ok
hello!

ハンドラのタイムアウトを設定する

どのリクエストも一律同じ時間でタイムアウトさせれば良い場合、http.TimeoutHandler()を使えば、一定時間でクライアントに503 Service Unavailableを返すようになります。

タイムアウトが発生したかどうかは、http.ResponseWriterWrite()http.ErrHandlerTimeoutを返すかどうかで判断できます。

package main

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

type handler struct{}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    <-time.After(1 * time.Minute)
    w.WriteHeader(http.StatusOK)
    _, err := w.Write([]byte("hello!"))
    switch err {
    case http.ErrHandlerTimeout:
        log.Println("timeout")
    case nil:
        log.Println("ok")
    default:
        log.Println("err:", err)
    }
}

func main() {
    log.SetFlags(0)
    log.SetPrefix("server: ")
    var h handler
    http.Handle("/", http.TimeoutHandler(&h, 10*time.Second, "timeout!"))
    http.ListenAndServe(":8080", nil)
}

このサーバへリクエストを送ると、10秒後にタイムアウトして、エラーとしてクライアントへ返却されます。ただしhandler.ServeHTTP()は実行され続けているので、1分後に本来の処理が完了します。

$ go run server.go &
$ curl http://localhost:8080/
timeout!        # 10秒後
server: timeout # 1分後

このため、アプリケーションによっては、タイムアウトしていたらロールバックする等の処理が必要かもしれません。

その他のタイムアウト

http.TimeoutHandler()の他にも、net/httpには色々なタイムアウトがあります。Then complete guide to Go net/http timeoutsには、どのタイムアウトがどこにかかるのかなど詳しく書かれているので、がとても分かりやすくおすすめです。