Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 パッケージの TransportResponseHeaderTimeout を追加します。

関連するIssue: #3362

変更の背景

この変更の背景には、HTTPクライアントがサーバーからのレスポンスヘッダーを無限に待機してしまう可能性があったという問題があります。特に、リクエストボディの送信が完了した後、サーバーが何らかの理由でレスポンスヘッダーをすぐに返さない場合に、クライアントがハングアップしてしまう状況を避ける必要がありました。

従来のGoのHTTPクライアントでは、コネクション全体のタイムアウトやリクエスト全体のタイムアウトは設定できましたが、レスポンスヘッダーの受信に特化したタイムアウトは存在しませんでした。これにより、サーバーが遅延したり、応答しない場合に、クライアント側で適切なエラーハンドリングやリソース解放が困難になるという課題がありました。

ResponseHeaderTimeout の導入は、このような特定のフェーズでのタイムアウトを可能にすることで、より堅牢で制御可能なHTTPクライアントの挙動を実現することを目的としています。これにより、アプリケーションはサーバーの応答が遅い場合でも、無期限に待機することなく、タイムアウトエラーを適切に処理できるようになります。

前提知識の解説

HTTP/1.1 のリクエスト・レスポンスサイクル

HTTP/1.1における基本的な通信は、クライアントがリクエストを送信し、サーバーがレスポンスを返すというサイクルで構成されます。このサイクルは以下のフェーズに大別できます。

  1. コネクション確立: クライアントがサーバーとのTCPコネクションを確立します。
  2. リクエスト送信: クライアントがHTTPリクエストライン、ヘッダー、そしてオプションでリクエストボディをサーバーに送信します。
  3. レスポンス受信: サーバーが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.Secondtime.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 ステートメントを使用して実装されています。

  1. respHeaderTimer チャネルの導入: var respHeaderTimer <-chan time.Time という新しいチャネル変数が導入されました。これは、ResponseHeaderTimeout が設定されている場合に、そのタイムアウトを監視するためのチャネルです。

  2. タイムアウトチャネルの起動: pc.t.ResponseHeaderTimeout (つまり Transport.ResponseHeaderTimeout) がゼロより大きい場合、time.After(d) を使用して respHeaderTimer チャネルが初期化されます。これにより、指定された時間が経過するとこのチャネルに値が送信され、タイムアウトイベントが発生します。

    if d := pc.t.ResponseHeaderTimeout; d > 0 {
        respHeaderTimer = time.After(d)
    }
    
  3. 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.goTestTransportResponseHeaderTimeout という新しいテスト関数が追加されました。このテストは、ResponseHeaderTimeout が正しく機能することを確認します。

  • httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。
  • /fast エンドポイントは即座に応答を返します。
  • /slow エンドポイントは2秒間 time.Sleep を実行してから応答を返します。
  • TransportResponseHeaderTimeout500 * 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 の変更点

  1. Transport 構造体への ResponseHeaderTimeout フィールドの追加: このフィールドは time.Duration 型で、HTTPリクエストの送信が完了してからレスポンスヘッダーが完全に受信されるまでの最大許容時間を定義します。このタイムアウトは、レスポンスボディの読み取り時間には影響しません。これにより、ヘッダーの受信が遅い場合にのみタイムアウトを発生させることができます。

  2. persistConn.roundTrip メソッド内の respHeaderTimer の導入: persistConn.roundTrip は、単一のHTTPトランザクション(リクエストの送信からレスポンスの受信まで)を処理する内部関数です。この関数内で、respHeaderTimer という新しいチャネルが宣言されます。これは、ResponseHeaderTimeout が設定されている場合に、そのタイムアウトを監視するためのGoのチャネルです。

  3. respHeaderTimer の初期化: WaitResponse ループの冒頭で、pc.t.ResponseHeaderTimeout (つまり Transport の設定値) がゼロより大きい場合、time.After(d) を使って respHeaderTimer が初期化されます。time.After(d) は、指定された期間 d が経過した後に現在時刻を送信するチャネルを返します。これにより、タイムアウト期間が開始されます。

  4. 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 の変更点

  1. TestTransportResponseHeaderTimeout 関数の追加: このテスト関数は、ResponseHeaderTimeout が期待通りに機能することを確認するためのものです。
    • httptest.NewServer を使用して、テスト用のHTTPサーバーをセットアップします。このサーバーには、即座に応答する /fast エンドポイントと、2秒間遅延してから応答する /slow エンドポイントがあります。
    • TransportResponseHeaderTimeout500 * time.Millisecond (0.5秒) に設定した http.Client を作成します。
    • テストケースの配列を定義し、/fast へのリクエストが成功すること、/slow へのリクエストがタイムアウトエラーで失敗すること、そして再度 /fast へのリクエストが成功することを確認します。
    • strings.Contains(err.Error(), tt.wantErr) を使用して、返されたエラーメッセージが期待されるタイムアウトエラーメッセージを含んでいるかを検証します。

このテストは、ResponseHeaderTimeout が正しく設定され、サーバーの応答が遅い場合にタイムアウトエラーが適切に発生することを確認する重要な役割を果たします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • HTTP/1.1 の仕様に関する一般的な知識
  • Go言語のチャネルと select ステートメントに関する一般的な知識