[インデックス 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
ステートメントに関する一般的な知識