Go の http.Server でタイムアウトを設定する:ReadTimeout・WriteTimeout・IdleTimeout
http.ListenAndServe は手軽ですが、タイムアウトの設定ができません。本番環境では http.Server 構造体を使い、ReadTimeout・WriteTimeout・IdleTimeout を明示的に設定するのが鉄則です。
なぜタイムアウトが必要なのか
タイムアウトを設定しないサーバーは、スローロリス攻撃のような低速リクエストに対して脆弱になります。攻撃者がコネクションを開いたまま少しずつデータを送り続けると、サーバーのリソースが枯渇し、正常なリクエストを受け付けられなくなるのです。
Go の http.ListenAndServe はデフォルトですべてのタイムアウトがゼロ(無制限)に設定されています。開発中はそれで問題ありませんが、本番にそのまま出すのは危険です。
http.Server で 3 つのタイムアウトを設定する
http.Server 構造体には 3 種類のタイムアウトフィールドがあります。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
fmt.Println("サーバー起動: http://localhost:8080")
if err := server.ListenAndServe(); err != nil {
fmt.Println("サーバーエラー:", err)
}
}http.ListenAndServe(":8080", mux) の代わりに server.ListenAndServe() を呼びます。サーバーの設定を構造体にまとめることで、タイムアウト以外にも TLS 設定やエラーログの出力先などを細かく制御できるようになります。
各タイムアウトの役割
3 つのタイムアウトはそれぞれ異なるフェーズを担当しています。
| ReadTimeout | リクエストヘッダーとボディの読み取り完了までの時間 |
| WriteTimeout | レスポンスの書き込み完了までの時間 |
| IdleTimeout | Keep-Alive 接続で次のリクエストが来るまでの待機時間 |
ReadTimeout はクライアントがリクエストを送り終えるまでの制限時間です。大きなファイルをアップロードする API では、この値を長めに設定する必要があります。短すぎると、正常なリクエストまでタイムアウトしてしまいます。
WriteTimeout はサーバーがレスポンスを返し終えるまでの制限時間になります。重い処理を行うエンドポイントでは余裕を持たせましょう。ただし、長すぎるとスロークライアントがコネクションを占有し続ける原因になります。
IdleTimeout は Keep-Alive 接続に関する設定です。HTTP/1.1 ではデフォルトで接続が維持されるため、この値が短すぎると頻繁に再接続が発生し、長すぎるとアイドル接続がリソースを消費し続けます。
クライアントが接続
ReadTimeout 内にリクエスト送信完了
WriteTimeout 内にレスポンス返却完了
IdleTimeout 内に次のリクエストが来なければ切断
推奨値の目安
タイムアウトの適切な値はアプリケーションの特性によって変わりますが、一般的な Web API であれば以下が出発点になります。
| タイムアウト | 推奨値 | 備考 |
|---|---|---|
| ReadTimeout | 5〜10 秒 | ファイルアップロードがあれば長めに |
| WriteTimeout | 10〜30 秒 | 重い処理があれば長めに |
| IdleTimeout | 60〜120 秒 | リバースプロキシ側の設定と合わせる |
リバースプロキシ(nginx など)を前段に置いている場合は、プロキシ側のタイムアウトとの整合性も考慮が必要です。Go 側の WriteTimeout がプロキシのタイムアウトより短いと、Go がレスポンスを返す前にプロキシが接続を切ってしまうことがあります。
ReadHeaderTimeout もある
Go 1.8 から ReadHeaderTimeout というフィールドも追加されています。これはリクエストヘッダーの読み取りだけに特化したタイムアウトです。
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 3 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}スローロリス攻撃への防御には ReadHeaderTimeout がもっとも効果的です。ヘッダーの読み取りはボディに比べて短時間で完了するはずなので、3 秒程度で十分でしょう。
手軽だがタイムアウトがすべて無制限。開発やプロトタイプ向き。
タイムアウトや TLS を細かく制御可能。本番運用では必須。
本番サーバーでは http.Server を使ってタイムアウトを設定する。これは Go の Web 開発における基本的なプラクティスとして覚えておいてください。