[インデックス 15466] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http パッケージの Transport 型に ResponseHeaderTimeout フィールドを追加するものです。これにより、HTTPクライアントがサーバーからのレスポンスヘッダーを待機する際のタイムアウトを設定できるようになります。
コミット
commit 839d47add56515fc9f127e60398ea7132f0b1d38
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 27 08:47:08 2013 -0800
net/http: add Transport.ResponseHeaderTimeout
Update #3362
R=golang-dev, adg, rsc
CC=golang-dev
https://golang.org/cl/7369055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/839d47add56515fc9f127e60398ea7132f0b1d38
元コミット内容
net/http: add Transport.ResponseHeaderTimeout
このコミットは、net/http パッケージの Transport に ResponseHeaderTimeout を追加します。
関連するIssue: #3362
変更の背景
この変更の背景には、HTTPクライアントがサーバーからのレスポンスヘッダーを無限に待機してしまう可能性があったという問題があります。特に、リクエストボディの送信が完了した後、サーバーが何らかの理由でレスポンスヘッダーをすぐに返さない場合に、クライアントがハングアップしてしまう状況を避ける必要がありました。
従来のGoのHTTPクライアントでは、コネクション全体のタイムアウトやリクエスト全体のタイムアウトは設定できましたが、レスポンスヘッダーの受信に特化したタイムアウトは存在しませんでした。これにより、サーバーが遅延したり、応答しない場合に、クライアント側で適切なエラーハンドリングやリソース解放が困難になるという課題がありました。
ResponseHeaderTimeout の導入は、このような特定のフェーズでのタイムアウトを可能にすることで、より堅牢で制御可能なHTTPクライアントの挙動を実現することを目的としています。これにより、アプリケーションはサーバーの応答が遅い場合でも、無期限に待機することなく、タイムアウトエラーを適切に処理できるようになります。
前提知識の解説
HTTP/1.1 のリクエスト・レスポンスサイクル
HTTP/1.1における基本的な通信は、クライアントがリクエストを送信し、サーバーがレスポンスを返すというサイクルで構成されます。このサイクルは以下のフェーズに大別できます。
- コネクション確立: クライアントがサーバーとのTCPコネクションを確立します。
- リクエスト送信: クライアントがHTTPリクエストライン、ヘッダー、そしてオプションでリクエストボディをサーバーに送信します。
- レスポンス受信: サーバーがHTTPステータスライン、ヘッダー、そしてオプションでレスポンスボディをクライアントに送信します。
このコミットで追加される ResponseHeaderTimeout は、上記3番目の「レスポンス受信」フェーズのうち、特に「HTTPステータスラインとヘッダー」の受信に焦点を当てたタイムアウトです。リクエストボディの送信が完了した後から、レスポンスヘッダーが完全に受信されるまでの時間を計測します。
Go言語の net/http パッケージ
net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。
http.Client: HTTPリクエストを送信するための高レベルなインターフェースを提供します。通常、ユーザーはこの型を通じてHTTP通信を行います。http.Transport:http.Clientの内部で実際にネットワーク通信を行う低レベルな実装です。コネクションの確立、リクエストの送信、レスポンスの受信、コネクションプーリングなどを担当します。Transportは、TCPコネクションの再利用(Keep-Alive)やプロキシ設定、TLS設定など、より詳細なネットワーク挙動を制御するための設定を提供します。
このコミットは http.Transport に変更を加えることで、http.Client を利用するすべてのHTTPリクエストに影響を与えるように設計されています。
time.Duration
Go言語の time パッケージで定義されている time.Duration 型は、時間の長さを表す型です。ナノ秒単位で時間を表現し、time.Second や time.Millisecond などの定数と組み合わせて、人間が読みやすい形で時間を指定できます(例: 500 * time.Millisecond)。
select ステートメントとチャネル
Go言語の select ステートメントは、複数の通信操作(チャネルの送受信)を同時に待機するために使用されます。いずれかのチャネル操作が可能になると、その操作が実行されます。複数のチャネル操作が同時に可能な場合は、ランダムに1つが選択されます。
time.After(d) は、指定された期間 d が経過した後に現在時刻を送信するチャネルを返します。これは、タイムアウトを実装する際によく使用されるイディオムです。
技術的詳細
このコミットは、net/http パッケージの Transport 型に ResponseHeaderTimeout フィールドを追加し、そのタイムアウトロジックを persistConn.roundTrip メソッドに組み込むことで、レスポンスヘッダーの受信タイムアウト機能を実現しています。
Transport.ResponseHeaderTimeout フィールドの追加
http.Transport 構造体に ResponseHeaderTimeout time.Duration フィールドが追加されました。このフィールドは、リクエストの送信が完了した後、サーバーからのレスポンスヘッダーを待機する最大時間を指定します。この時間には、レスポンスボディの読み取り時間は含まれません。値がゼロの場合、タイムアウトは適用されません。
type Transport struct {
// ... 既存のフィールド ...
// ResponseHeaderTimeout, if non-zero, specifies the amount of
// time to wait for a server's response headers after fully
// writing the request (including its body, if any). This
// time does not include the time to read the response body.
ResponseHeaderTimeout time.Duration
}
persistConn.roundTrip メソッドでのタイムアウト処理
persistConn.roundTrip メソッドは、単一のHTTPリクエスト/レスポンスサイクルを処理する内部メソッドです。このメソッド内で、ResponseHeaderTimeout のロジックが select ステートメントを使用して実装されています。
-
respHeaderTimerチャネルの導入:var respHeaderTimer <-chan time.Timeという新しいチャネル変数が導入されました。これは、ResponseHeaderTimeoutが設定されている場合に、そのタイムアウトを監視するためのチャネルです。 -
タイムアウトチャネルの起動:
pc.t.ResponseHeaderTimeout(つまりTransport.ResponseHeaderTimeout) がゼロより大きい場合、time.After(d)を使用してrespHeaderTimerチャネルが初期化されます。これにより、指定された時間が経過するとこのチャネルに値が送信され、タイムアウトイベントが発生します。if d := pc.t.ResponseHeaderTimeout; d > 0 { respHeaderTimer = time.After(d) } -
selectステートメントでのタイムアウト監視:persistConn.roundTripメソッド内のWaitResponseループのselectステートメントに、新しいcase <-respHeaderTimer:が追加されました。- このケースが選択された場合(つまり、
ResponseHeaderTimeoutが経過した場合)、pc.close()が呼び出され、現在の永続コネクションが閉じられます。 re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")}が設定され、適切なタイムアウトエラーが返されます。break WaitResponseによってループが終了し、エラーが呼び出し元に伝播されます。
case <-respHeaderTimer: pc.close() re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")} break WaitResponse - このケースが選択された場合(つまり、
テストケースの追加
src/pkg/net/http/transport_test.go に TestTransportResponseHeaderTimeout という新しいテスト関数が追加されました。このテストは、ResponseHeaderTimeout が正しく機能することを確認します。
httptest.NewServerを使用してテスト用のHTTPサーバーを起動します。/fastエンドポイントは即座に応答を返します。/slowエンドポイントは2秒間time.Sleepを実行してから応答を返します。TransportのResponseHeaderTimeoutを500 * time.Millisecond(0.5秒) に設定したhttp.Clientを作成します。/fastへのリクエストは成功することを確認します。/slowへのリクエストはtimeout awaiting response headersエラーで失敗することを確認します。- 再度
/fastへのリクエストが成功することを確認し、コネクションがタイムアウト後も再利用可能であることを間接的にテストします(ただし、このテストではコネクションが閉じられるため、再利用はされません)。
このテストは、ResponseHeaderTimeout が期待通りに機能し、遅延するサーバーからのレスポンスヘッダー受信時にタイムアウトエラーを発生させることを検証しています。
コアとなるコードの変更箇所
src/pkg/net/http/transport.go
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -73,6 +73,12 @@ type Transport struct {
// (keep-alive) to keep per-host. If zero,
// DefaultMaxIdleConnsPerHost is used.
MaxIdleConnsPerHost int
+
+ // ResponseHeaderTimeout, if non-zero, specifies the amount of
+ // time to wait for a server's response headers after fully
+ // writing the request (including its body, if any). This
+ // time does not include the time to read the response body.
+ ResponseHeaderTimeout time.Duration
}
// ProxyFromEnvironment returns the URL of the proxy to use for a
@@ -743,6 +749,7 @@ func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err err
var re responseAndError
var pconnDeadCh = pc.closech
var failTicker <-chan time.Time
+ var respHeaderTimer <-chan time.Time
WaitResponse:
for {
select {
@@ -752,6 +759,9 @@ WaitResponse:
pc.close()
break WaitResponse
}
+ if d := pc.t.ResponseHeaderTimeout; d > 0 {
+ respHeaderTimer = time.After(d)
+ }
case <-pconnDeadCh:
// The persist connection is dead. This shouldn't
// usually happen (only with Connection: close responses
@@ -768,7 +778,11 @@ WaitResponse:
pconnDeadCh = nil // avoid spinning
failTicker = time.After(100 * time.Millisecond) // arbitrary time to wait for resc
case <-failTicker:
- re = responseAndError{nil, errors.New("net/http: transport closed before response was received")}
+ re = responseAndError{err: errors.New("net/http: transport closed before response was received")}
+ break WaitResponse
+ case <-respHeaderTimer:
+ pc.close()
+ re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")}
break WaitResponse
case re = <-resc:
break WaitResponse
src/pkg/net/http/transport_test.go
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1113,6 +1113,54 @@ func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) {
ts.Close()
}
+func TestTransportResponseHeaderTimeout(t *testing.T) {
+ defer checkLeakedTransports(t)
+ if testing.Short() {
+ t.Skip("skipping timeout test in -short mode")
+ }
+ const debug = false
+ mux := NewServeMux()
+ mux.HandleFunc("/fast", func(w ResponseWriter, r *Request) {})
+ mux.HandleFunc("/slow", func(w ResponseWriter, r *Request) {
+ time.Sleep(2 * time.Second)
+ })
+ ts := httptest.NewServer(mux)
+ defer ts.Close()
+
+ tr := &Transport{
+ ResponseHeaderTimeout: 500 * time.Millisecond,
+ }
+ defer tr.CloseIdleConnections()
+ c := &Client{Transport: tr}
+
+ tests := []struct {
+ path string
+ want int
+ wantErr string
+ }{
+ {path: "/fast", want: 200},
+ {path: "/slow", wantErr: "timeout awaiting response headers"},
+ {path: "/fast", want: 200},
+ }
+ for i, tt := range tests {
+ res, err := c.Get(ts.URL + tt.path)
+ if err != nil {
+ if strings.Contains(err.Error(), tt.wantErr) {
+ continue
+ }
+ t.Errorf("%d. unexpected error: %v", i, err)
+ continue
+ }
+ if tt.wantErr != "" {
+ t.Errorf("%d. no error. expected error: %v", i, tt.wantErr)
+ continue
+ }
+ if res.StatusCode != tt.want {
+ t.Errorf("%d for path %q status = %d; want %d", i, tt.path, res.StatusCode, tt.want)
+ }
+ }
+}
+
type fooProto struct{}
func (fooProto) RoundTrip(req *Request) (*Response, error) {
コアとなるコードの解説
src/pkg/net/http/transport.go の変更点
-
Transport構造体へのResponseHeaderTimeoutフィールドの追加: このフィールドはtime.Duration型で、HTTPリクエストの送信が完了してからレスポンスヘッダーが完全に受信されるまでの最大許容時間を定義します。このタイムアウトは、レスポンスボディの読み取り時間には影響しません。これにより、ヘッダーの受信が遅い場合にのみタイムアウトを発生させることができます。 -
persistConn.roundTripメソッド内のrespHeaderTimerの導入:persistConn.roundTripは、単一のHTTPトランザクション(リクエストの送信からレスポンスの受信まで)を処理する内部関数です。この関数内で、respHeaderTimerという新しいチャネルが宣言されます。これは、ResponseHeaderTimeoutが設定されている場合に、そのタイムアウトを監視するためのGoのチャネルです。 -
respHeaderTimerの初期化:WaitResponseループの冒頭で、pc.t.ResponseHeaderTimeout(つまりTransportの設定値) がゼロより大きい場合、time.After(d)を使ってrespHeaderTimerが初期化されます。time.After(d)は、指定された期間dが経過した後に現在時刻を送信するチャネルを返します。これにより、タイムアウト期間が開始されます。 -
selectステートメントへのrespHeaderTimerの追加:WaitResponseループ内のselectステートメントに、case <-respHeaderTimer:という新しいケースが追加されました。- このケースがトリガーされる(つまり、
ResponseHeaderTimeoutが経過する)と、pc.close()が呼び出され、現在の永続コネクションが閉じられます。これは、タイムアウトしたコネクションを再利用しないようにするためです。 re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")}が設定され、"net/http: timeout awaiting response headers"という具体的なエラーメッセージを含むresponseAndError構造体が作成されます。このエラーは、呼び出し元に返され、レスポンスヘッダーのタイムアウトが発生したことを示します。break WaitResponseによってループが終了し、エラーが伝播されます。
- このケースがトリガーされる(つまり、
この変更により、HTTPクライアントは、リクエストボディの送信が完了した後、サーバーからのレスポンスヘッダーの受信に特定の時間制限を設けることができるようになり、よりきめ細やかなタイムアウト制御が可能になりました。
src/pkg/net/http/transport_test.go の変更点
TestTransportResponseHeaderTimeout関数の追加: このテスト関数は、ResponseHeaderTimeoutが期待通りに機能することを確認するためのものです。httptest.NewServerを使用して、テスト用のHTTPサーバーをセットアップします。このサーバーには、即座に応答する/fastエンドポイントと、2秒間遅延してから応答する/slowエンドポイントがあります。TransportのResponseHeaderTimeoutを500 * time.Millisecond(0.5秒) に設定したhttp.Clientを作成します。- テストケースの配列を定義し、
/fastへのリクエストが成功すること、/slowへのリクエストがタイムアウトエラーで失敗すること、そして再度/fastへのリクエストが成功することを確認します。 strings.Contains(err.Error(), tt.wantErr)を使用して、返されたエラーメッセージが期待されるタイムアウトエラーメッセージを含んでいるかを検証します。
このテストは、ResponseHeaderTimeout が正しく設定され、サーバーの応答が遅い場合にタイムアウトエラーが適切に発生することを確認する重要な役割を果たします。
関連リンク
- Go言語の
net/httpパッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
timeパッケージのドキュメント: https://pkg.go.dev/time
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- HTTP/1.1 の仕様に関する一般的な知識
- Go言語のチャネルと
selectステートメントに関する一般的な知識