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

[インデックス 16027] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージの Transport コンポーネントにおける、HTTP 1xx(情報提供)レスポンスの取り扱いに関する変更です。特に、HTTP 100 (Continue) 以外の1xxレスポンスを受け取った際に、TCP接続を再利用しないようにする(Keep-Aliveを無効にする)ことで、将来的なプロトコル拡張や予期せぬ動作に対する堅牢性を高めることを目的としています。

コミット

  • コミットハッシュ: b80ce2034bd04b23cb9b4330aea3d390b2f0df3a
  • 作者: Brad Fitzpatrick bradfitz@golang.org
  • 日付: 2013年3月30日 (土) 22:59:08 -0700

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/b80ce2034bd04b23cb9b4330aea3d390b2f0df3a

元コミット内容

net/http: Transport: be paranoid about any non-100 1xx response

Since we can't properly handle anything except 100, treat all
1xx informational responses as sketchy and don't reuse the
connection for future requests.

The only other 1xx response code currently in use in the wild
is WebSockets' use of "101 Switching Protocols", but our
code.google.com/p/go.net/websockets doesn't use Client or
Transport: it uses ReadResponse directly, so is unaffected by
this CL.  (and its tests still pass)

So this CL is entirely just future-proofing paranoia.
Also: the Internet is weird.

Update #2184
Update #3665

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/8208043

変更の背景

HTTPプロトコルにおいて、1xxステータスコードは「情報提供 (Informational)」レスポンスとして定義されています。最も一般的なのは 100 Continue で、これはクライアントが大きなリクエストボディを送信する前に、サーバーがリクエストヘッダを受け入れ可能であることを確認するために使用されます。しかし、HTTP/1.1の仕様では 101 Switching Protocols 以外にも 102 Processing (WebDAV) や 103 Early Hints (HTTP/2, HTTP/3) など、様々な1xxステータスコードが存在し得ます。

Goの net/http パッケージの Transport は、HTTPクライアントがサーバーと通信するための低レベルな詳細を処理します。これには、TCP接続の確立、リクエストの送信、レスポンスの受信、そしてパフォーマンス向上のための接続の再利用(Keep-Alive)が含まれます。

このコミットが作成された時点では、net/http パッケージは 100 Continue 以外の1xxレスポンスを適切に処理するようには設計されていませんでした。もし未知の、あるいは予期せぬ1xxレスポンスを受け取った場合、Transport がその後の通信で同じTCP接続を再利用すると、プロトコル状態の不整合や予期せぬエラーが発生する可能性がありました。

この変更の背景には、「将来的なプロトコル拡張や、HTTP仕様に厳密に従わない(あるいは未定義の)サーバーからの予期せぬ1xxレスポンスに対する堅牢性の向上」という意図があります。特に、101 Switching Protocols はWebSocketsなどで使用されますが、GoのWebSocketsライブラリは net/http.ClientTransport を直接使用せず、より低レベルな ReadResponse を用いるため、この変更の影響を受けません。したがって、このコミットは「将来を見越した予防的な対策(future-proofing paranoia)」として位置づけられています。

前提知識の解説

HTTP 1xx ステータスコード

HTTPステータスコードは、HTTPリクエストの結果を示す3桁の数字です。1xxの範囲は「情報提供 (Informational)」レスポンスとして予約されており、リクエストが受信され、処理が継続中であることを示します。

  • 100 Continue: クライアントがリクエストボディを送信し続けるべきであることを示します。クライアントは Expect: 100-continue ヘッダを送信し、サーバーがこれを受け入れ可能であれば 100 Continue を返します。
  • 101 Switching Protocols: クライアントが Upgrade ヘッダを送信し、サーバーがプロトコルを切り替えることを同意したことを示します。WebSocketsのハンドシェイクでよく使用されます。
  • 102 Processing (WebDAV): サーバーがリクエストを処理中であり、まだ最終的なレスポンスを返す準備ができていないことを示します。
  • 103 Early Hints (HTTP/2, HTTP/3): サーバーが最終的なレスポンスを返す前に、クライアントにリソースヒント(例: Link ヘッダ)を早期に提供するために使用されます。

これらの1xxレスポンスは、最終的な2xx、3xx、4xx、5xxレスポンスの前に送信される一時的なものです。

HTTP Persistent Connections (Keep-Alive)

HTTP/1.1では、デフォルトで「永続的な接続(Persistent Connections)」、一般に「Keep-Alive」と呼ばれる機能が有効になっています。これは、一つのTCP接続上で複数のHTTPリクエストとレスポンスを連続して送受信することを可能にするものです。

Keep-Aliveの利点:

  • レイテンシの削減: 各リクエストごとに新しいTCP接続を確立するオーバーヘッド(TCPハンドシェイク、SSL/TLSハンドシェイクなど)を削減します。
  • ネットワークリソースの効率化: 接続の確立と切断にかかるリソースを節約します。

net/http.Transport は、このKeep-Alive機能を管理し、アイドル状態の接続をプールして再利用することで、クライアントのパフォーマンスを向上させます。しかし、接続を再利用するためには、その接続が健全な状態であり、次のリクエストを正しく処理できるプロトコル状態にあることが前提となります。

Go言語の net/http パッケージ

Goの net/http パッケージは、HTTPクライアントとサーバーを実装するための強力な機能を提供します。

  • http.Client: HTTPリクエストを送信するための高レベルなインターフェースを提供します。
  • http.Transport: http.Client の内部で使用され、実際のネットワーク通信(TCP接続の管理、TLSハンドシェイク、プロキシの処理、Keep-Aliveなど)を担当します。Transport は、複数のリクエスト間でTCP接続を再利用するための接続プールを管理します。
  • persistConn: Transport の内部で、単一の永続的なTCP接続を表現する構造体です。この構造体が、接続の読み書き、Keep-Aliveの状態管理、そして接続の再利用可能性を決定します。

技術的詳細

このコミットの核心は、net/http/transport.go 内の persistConn.readLoop() メソッドにおける接続再利用のロジック変更です。

persistConn.readLoop() は、サーバーからのレスポンスを読み取り、その接続を再利用できるかどうかを判断する役割を担っています。以前のバージョンでは、接続を再利用しない条件は以下のいずれかでした。

  1. レスポンスの読み取り中にエラーが発生した場合 (err != nil)
  2. サーバーが接続を閉じることを要求した場合 (resp.Close)
  3. クライアントがリクエストで接続を閉じることを要求した場合 (rc.req.Close)

このコミットでは、上記の条件に加えて、resp.StatusCode <= 199 という条件が追加されました。これは、HTTPステータスコードが1xxの範囲(100から199まで)である場合に、その接続を再利用しないことを意味します。

なぜ resp.StatusCode <= 199 なのか?

  • 100 Continueの特殊性: 100 Continue は、リクエストボディの送信を継続するためのものであり、Transport はこれを特別に処理します。このコミットの変更は、100 Continue が既に適切に処理されていることを前提としています。
  • 非100の1xxレスポンスの不確実性: 100 Continue 以外の1xxレスポンスは、net/httpTransport がその後の通信で接続を安全に再利用できるようなプロトコル状態を保証しません。例えば、101 Switching Protocols はプロトコルがHTTPから別のものに切り替わったことを意味するため、同じHTTP接続を再利用することはできません。その他の未知の1xxレスポンスも同様に、Transport が予期しない状態を引き起こす可能性があります。
  • 「偏執的な」未来対応: コミットメッセージにあるように、「future-proofing paranoia」(将来を見越した偏執的な対策)として、100 Continue 以外のすべての1xxレスポンスを「怪しい (sketchy)」ものとして扱い、接続の再利用を避けることで、未知のプロトコル拡張やサーバーの誤動作に対する堅牢性を高めています。

この変更により、Transport100 Continue 以外の1xxレスポンスを受け取った場合、その接続をプールに戻さず、閉じることになります。これにより、後続のリクエストが予期せぬプロトコル状態の接続を使用することを防ぎます。

テストコード (src/pkg/net/http/transport_test.go) では、123 Sesame Street という架空の1xxステータスコードを返すサーバーをシミュレートし、その際に接続が再利用されないことを確認するテストが追加されています。これは、この変更が意図通りに機能していることを検証するためのものです。

コアとなるコードの変更箇所

src/pkg/net/http/transport.go

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -715,7 +715,10 @@ func (pc *persistConn) readLoop() {
 			resp.Body = &bodyEOFSignal{body: resp.Body}
 		}
 
-		if err != nil || resp.Close || rc.req.Close {
+		if err != nil || resp.Close || rc.req.Close || resp.StatusCode <= 199 {
+			// Don't do keep-alive on error if either party requested a close
+			// or we get an unexpected informational (1xx) response.
+			// StatusCode 100 is already handled above.
 			alive = false
 		}

src/pkg/net/http/transport_test.go

テストケース TestTransportReading100Continue に、X-Want-Response-Code ヘッダを使ってサーバーに 123 Sesame Street というステータスコードを返させ、その際に接続が再利用されないことを確認するロジックが追加されています。

--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1474,6 +1474,13 @@ func TestTransportReading100Continue(t *testing.T) {
 	for i := 1; i <= numReqs; i++ {
 		req, _ := NewRequest("POST", "http://other.tld/", strings.NewReader(reqBody(i)))
 		req.Header.Set("X-Want-Response-Code", "123 Sesame Street")
 		testResponse(req, fmt.Sprintf("123, %d/%d", i, numReqs), 123)
 	}
 }

src/pkg/net/http/serve_test.go

テスト用の rwTestConn 構造体に closeFunc フィールドが追加され、接続のクローズ時にカスタム関数を実行できるようになりました。これは transport_test.go での新しいテストシナリオをサポートするための補助的な変更です。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -77,10 +77,15 @@ type rwTestConn struct {
 	io.Reader
 	io.Writer
 	noopConn
-	closec chan bool // if non-nil, send value to it on close
+
+	closeFunc func() error // called if non-nil
+	closec    chan bool    // else, if non-nil, send value to it on close
 }
 
 func (c *rwTestConn) Close() error {
+	if c.closeFunc != nil {
+		return c.closeFunc()
+	}
 	select {
 	case c.closec <- true:
 	default:

コアとなるコードの解説

src/pkg/net/http/transport.gopersistConn.readLoop() メソッド内の以下の行が、この変更の核心です。

		if err != nil || resp.Close || rc.req.Close || resp.StatusCode <= 199 {
			// Don't do keep-alive on error if either party requested a close
			// or we get an unexpected informational (1xx) response.
			// StatusCode 100 is already handled above.
			alive = false
		}

この if 文は、レスポンスを読み取った後に、現在の persistConn (永続的なTCP接続) を後続のリクエストのために再利用すべきかどうか (alive 変数で制御) を決定します。

  • err != nil: レスポンスの読み取り中にネットワークエラーやプロトコルエラーが発生した場合。接続は破損している可能性が高いため、再利用しません。
  • resp.Close: サーバーがレスポンスヘッダに Connection: close を含めるなどして、接続を閉じることを明示的に要求した場合。
  • rc.req.Close: クライアントがリクエストヘッダに Connection: close を含めるなどして、接続を閉じることを明示的に要求した場合。
  • resp.StatusCode <= 199: このコミットで追加された条件。 レスポンスのHTTPステータスコードが100番台(1xx)である場合。
    • コメントにあるように、「StatusCode 100 is already handled above.」とあるため、この条件は実質的に 101 から 199 までのステータスコードを対象としています。
    • これらの情報提供レスポンスは、最終的なレスポンスの前に送信される一時的なものであり、net/http.Transport100 Continue 以外を適切に処理し、その後の接続再利用を安全に行える保証がないため、接続を再利用しない判断が下されます。

alive = false が設定されると、readLoop は現在の接続を接続プールに戻さず、接続は閉じられます。これにより、将来のリクエストは新しいTCP接続を確立することになり、予期せぬ1xxレスポンスによって引き起こされる可能性のあるプロトコル状態の不整合を防ぎます。

関連リンク

参考にした情報源リンク