[インデックス 10308] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおいて、HTTPサーバーのチャンク転送エンコーディングに関する既知のバグ(Issue 2357)を再現するためのテストケースを追加するものです。このテストは、バグが修正されるまで意図的に無効化されています。
コミット
- コミットハッシュ:
21f5057639a1ca81b705307c1ed8c0af1249a308
- 作者: Brad Fitzpatrick bradfitz@golang.org
- コミット日時: 2011年11月9日 水曜日 08:12:26 -0800
- 変更ファイル:
src/pkg/net/http/serve_test.go
- 変更概要:
TestServerBufferedChunking
という新しいテスト関数を追加。このテストは、HTTPサーバーがチャンクエンコーディングでレスポンスを送信する際に、小さな書き込み(1バイトずつなど)がチャンクヘッダーの追加前に適切にバッファリングされることを検証しようとします。しかし、当時の実装ではこの動作が正しくなかったため、テストは「既知の壊れたテスト」として無効化されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21f5057639a1ca81b705307c1ed8c0af1249a308
元コミット内容
http: add a disabled failing test for Issue 2357
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5372044
変更の背景
このコミットの背景には、Goの net/http
パッケージにおけるHTTPチャンク転送エンコーディングの実装に関するバグ、具体的には Issue 2357: http: chunked server responses that write 1 byte at a time are buffered before chunk headers are added, not after chunk headers があります。
問題の核心は、HTTPサーバーがチャンク転送エンコーディングを使用してレスポンスを送信する際に、アプリケーションが ResponseWriter.Write
メソッドを複数回呼び出し、それぞれが非常に小さなデータ(例えば1バイト)を書き込む場合に発生していました。理想的には、これらの小さな書き込みは、チャンクヘッダー(チャンクのサイズを示す部分)が追加される前に内部的にバッファリングされ、ある程度のデータがまとまってから一つのチャンクとして送信されるべきです。これにより、ネットワーク効率が向上し、不必要なオーバーヘッドが削減されます。
しかし、当時の net/http
の実装では、このバッファリングがチャンクヘッダーの追加後に行われてしまう、あるいは全く行われないという問題がありました。その結果、1バイトのデータごとにチャンクヘッダーが追加され、1\r\nx\r\n1\r\ny\r\n1\r\nz\r\n
のように非常に非効率な形でデータが送信されていました。これは、HTTP/1.1のチャンク転送エンコーディングの目的(動的に生成されるコンテンツのサイズを事前に知ることなく送信できること)を損なうものではありませんが、パフォーマンスの観点からは望ましくありませんでした。
このコミットは、このバグの存在を明確にし、将来的な修正を促すために、この特定の非効率な動作を再現するテストケースを追加しました。テストは、バグが修正されるまで「既知の壊れたテスト」として無効化されています。
前提知識の解説
HTTP チャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に、メッセージを複数の「チャンク」に分割して送信するメカニズムとしてチャンク転送エンコーディングが導入されました。これは、特に動的に生成されるコンテンツや、大きなファイルをストリーミングする際に有用です。
- 形式: 各チャンクは、そのチャンクのサイズ(16進数)と、それに続くデータ、そしてCRLF(
\r\n
)で構成されます。 - 終端: 最後のチャンクはサイズが0のチャンク(
0\r\n
)で示され、その後にトレーラーヘッダー(オプション)と最終的なCRLFが続きます。 - 目的:
Content-Length
ヘッダーを事前に計算できない場合でも、HTTP接続を維持したままレスポンスを送信できるようにします。
このコミットの文脈では、サーバーが rw.Write([]byte{'x'})
, rw.Write([]byte{'y'})
, rw.Write([]byte{'z'})
のように個別に1バイトずつ書き込んだときに、これらが 3\r\nxyz\r\n
のように単一のチャンクとしてバッファリングされて送信されるべきか、それとも 1\r\nx\r\n1\r\ny\r\n1\r\nz\r\n
のように個別のチャンクとして送信されるべきか、という点が問題となります。効率を考えると前者が望ましい動作です。
Go言語の net/http
パッケージ
Goの net/http
パッケージは、HTTPクライアントとサーバーを実装するための強力な機能を提供します。
http.Serve(l net.Listener, handler http.Handler)
: 指定されたnet.Listener
からの接続を受け入れ、それぞれの接続に対してhandler
を呼び出してHTTPリクエストを処理します。http.Handler
インターフェース:ServeHTTP(ResponseWriter, *Request)
メソッドを持つインターフェースです。HTTPリクエストを処理するすべてのハンドラはこのインターフェースを実装します。http.HandlerFunc
: 関数をhttp.Handler
インターフェースに適合させるためのアダプター型です。これにより、通常の関数をHTTPハンドラとして使用できます。http.ResponseWriter
インターフェース: HTTPレスポンスを構築するためにハンドラが使用するインターフェースです。Header() Header
: レスポンスヘッダーを返します。Write([]byte) (int, error)
: レスポンスボディにデータを書き込みます。WriteHeader(statusCode int)
: HTTPステータスコードを書き込みます。
http.Request
: 受信したHTTPリクエストを表す構造体です。
テストユーティリティ
testConn
:net/http
パッケージのテストで使用されるカスタムのnet.Conn
実装です。実際のネットワーク接続の代わりに、メモリ上のバッファ(readBuf
とwriteBuf
)を使用してデータの読み書きをシミュレートします。これにより、ネットワークI/Oを伴わずにHTTPサーバーの動作をテストできます。oneConnListener
:net.Listener
インターフェースのカスタム実装で、一度だけ接続(testConn
)を受け入れるように設計されています。これにより、単一のHTTPリクエスト/レスポンスサイクルをテストできます。bytes.HasSuffix(s, suffix []byte) bool
:bytes
パッケージの関数で、バイトスライスs
がバイトスライスsuffix
で終わるかどうかをチェックします。テストでは、レスポンスボディが期待されるチャンク形式で終わっているかを確認するために使用されます。
技術的詳細
追加された TestServerBufferedChunking
テストは、net/http
サーバーがチャンク転送エンコーディングを使用する際のバッファリング動作を検証することを目的としています。
-
テストの無効化:
if true { t.Logf("Skipping known broken test; see Issue 2357") return }
この
if true
ブロックにより、テストは常にスキップされます。これは、テストが当時のnet/http
の実装では失敗することが分かっていたためです。この記述は、バグが修正された際にこのブロックを削除し、テストを有効化するためのプレースホルダーとして機能します。 -
testConn
のセットアップ:conn := new(testConn) conn.readBuf.Write([]byte("GET / HTTP/1.1\\r\\n\\r\\n"))
testConn
のインスタンスを作成し、そのreadBuf
にシンプルなHTTP GETリクエスト(GET / HTTP/1.1\r\n\r\n
)を書き込みます。これは、サーバーが読み取るクライアントからのリクエストをシミュレートします。 -
oneConnListener
のセットアップ:done := make(chan bool) ls := &oneConnListener{conn}
テストの完了を通知するためのチャネル
done
と、testConn
をラップするoneConnListener
を作成します。 -
HTTPサーバーの起動とハンドラの定義:
go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) { defer close(done) rw.Header().Set("Content-Type", "text/plain") // prevent sniffing, which buffers rw.Write([]byte{'x'}) rw.Write([]byte{'y'}) rw.Write([]byte{'z'}) }))
新しいゴルーチンで
http.Serve
を起動します。このサーバーはoneConnListener
から接続を受け入れ、定義されたHandlerFunc
を使用してリクエストを処理します。defer close(done)
: ハンドラが完了したらdone
チャネルを閉じ、メインゴルーチンに処理の完了を通知します。rw.Header().Set("Content-Type", "text/plain")
:Content-Type
ヘッダーを設定します。コメントにあるように、これは「スニッフィング(コンテンツタイプの自動検出)を防ぎ、それによってバッファリングされるのを防ぐ」ためです。つまり、このヘッダーを設定することで、net/http
がレスポンスボディを自動的にバッファリングしてContent-Length
ヘッダーを追加するような最適化を回避し、チャンク転送エンコーディングが強制されるようにします。rw.Write([]byte{'x'})
,rw.Write([]byte{'y'})
,rw.Write([]byte{'z'})
: ここがテストの核心です。ハンドラは3回に分けて、それぞれ1バイトのデータをレスポンスボディに書き込みます。
-
テスト結果の検証:
<-done if !bytes.HasSuffix(conn.writeBuf.Bytes(), []byte("\\r\\n\\r\\n3\\r\\nxyz\\r\\n0\\r\\n\\r\\n")) { t.Errorf("response didn't end with a single 3 byte 'xyz' chunk; got:\\n%q", conn.writeBuf.Bytes()) }
done
チャネルからの受信を待ち、ハンドラの完了を待ちます。その後、testConn
のwriteBuf
に書き込まれたサーバーのレスポンスを検証します。- 期待される結果は、
\r\n\r\n3\r\nxyz\r\n0\r\n\r\n
で終わることです。これは、3バイトのデータ (xyz
) が単一のチャンクとしてバッファリングされ、そのチャンクのサイズ (3
) がヘッダーとして付加され、その後に終端チャンク (0
) が続く形式です。 - もしこの形式でなければ、テストは失敗し、エラーメッセージと実際のレスポンス内容が出力されます。
- 期待される結果は、
このテストは、rw.Write
が複数回呼び出されたときに、net/http
がチャンクヘッダーを効率的に管理し、小さな書き込みをまとめて一つのチャンクとして送信するべきであるという期待を表現しています。当時の実装ではこれができていなかったため、テストは失敗するはずでした。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1077,6 +1077,31 @@ func TestClientWriteShutdown(t *testing.T) {
}
}
+// Tests that chunked server responses that write 1 byte at a time are
+// buffered before chunk headers are added, not after chunk headers.
+func TestServerBufferedChunking(t *testing.T) {
+ if true {
+ t.Logf("Skipping known broken test; see Issue 2357")
+ return
+ }
+ conn := new(testConn)
+ conn.readBuf.Write([]byte("GET / HTTP/1.1\\r\\n\\r\\n"))
+ done := make(chan bool)
+ ls := &oneConnListener{conn}
+ go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) {
+ defer close(done)
+ rw.Header().Set("Content-Type", "text/plain") // prevent sniffing, which buffers
+ rw.Write([]byte{'x'})
+ rw.Write([]byte{'y'})
+ rw.Write([]byte{'z'})
+ }))
+ <-done
+ if !bytes.HasSuffix(conn.writeBuf.Bytes(), []byte("\\r\\n\\r\\n3\\r\\nxyz\\r\\n0\\r\\n\\r\\n")) {
+ t.Errorf("response didn't end with a single 3 byte 'xyz' chunk; got:\\n%q",
+ conn.writeBuf.Bytes())
+ }
+}
+
// goTimeout runs f, failing t if f takes more than ns to complete.
func goTimeout(t *testing.T, ns int64, f func()) {
ch := make(chan bool, 2)
コアとなるコードの解説
追加された TestServerBufferedChunking
関数は、Goのテストフレームワーク (testing
パッケージ) に基づいており、*testing.T
型の引数 t
を受け取ります。
-
テストのスキップ:
if true { t.Logf("Skipping known broken test; see Issue 2357") return }
このブロックは、テストが実行されるとすぐに
t.Logf
を使ってログメッセージを出力し、return
でテスト関数を終了させます。これにより、テストは実行されずにスキップされます。これは、このテストが当時のnet/http
の実装では失敗することが分かっていたため、CI/CDパイプラインなどで常に失敗するテストとして残しておくのではなく、一時的に無効化するための一般的なパターンです。Issue 2357が解決されたら、このif true
を削除してテストを有効化することが意図されています。 -
テスト接続の準備:
conn := new(testConn) conn.readBuf.Write([]byte("GET / HTTP/1.1\\r\\n\\r\\n"))
testConn
はnet.Conn
インターフェースを実装するテスト用の構造体で、実際のTCP接続の代わりにメモリ上のバッファ (readBuf
とwriteBuf
) を使用します。ここでは、クライアントからのリクエストをシミュレートするために、GET / HTTP/1.1\r\n\r\n
というHTTPリクエストをconn.readBuf
に書き込んでいます。 -
リスナーと完了チャネルの準備:
done := make(chan bool) ls := &oneConnListener{conn}
done
チャネルは、HTTPハンドラが処理を完了したことをメインのテストゴルーチンに通知するために使用されます。oneConnListener
はnet.Listener
インターフェースを実装するテスト用の構造体で、一度だけconn
を返すように設定されています。これにより、単一のHTTPリクエスト/レスポンスサイクルをテストできます。 -
HTTPサーバーの起動とハンドラの定義:
go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) { defer close(done) rw.Header().Set("Content-Type", "text/plain") // prevent sniffing, which buffers rw.Write([]byte{'x'}) rw.Write([]byte{'y'}) rw.Write([]byte{'z'}) }))
go Serve(...)
は、新しいゴルーチンでHTTPサーバーを起動します。ls
から接続を受け入れ、HandlerFunc
で定義された匿名関数が各リクエストを処理します。defer close(done)
: この行は、ハンドラ関数が終了する直前にdone
チャネルを閉じます。これにより、メインゴルーチンは<-done
でハンドラの完了を待つことができます。rw.Header().Set("Content-Type", "text/plain")
:ResponseWriter
のHeader()
メソッドを使ってレスポンスヘッダーを取得し、Content-Type
をtext/plain
に設定しています。これは、net/http
がレスポンスボディの内容を「スニッフィング」して自動的にContent-Length
ヘッダーを追加するのを防ぐためです。Content-Length
が設定されるとチャンク転送エンコーディングは使用されなくなるため、このテストの目的(チャンク転送のバッファリング動作の検証)を達成するために重要です。rw.Write([]byte{'x'})
,rw.Write([]byte{'y'})
,rw.Write([]byte{'z'})
: ここがテストの肝となる部分です。ハンドラは、それぞれ1バイトのデータを3回に分けてResponseWriter
に書き込んでいます。このテストの目的は、これらの小さな書き込みが、チャンクヘッダーが追加される前に内部的にバッファリングされ、最終的に3\r\nxyz\r\n
のように単一のチャンクとして送信されることを確認することです。
-
結果の検証:
<-done if !bytes.HasSuffix(conn.writeBuf.Bytes(), []byte("\\r\\n\\r\\n3\\r\\nxyz\\r\\n0\\r\\n\\r\\n")) { t.Errorf("response didn't end with a single 3 byte 'xyz' chunk; got:\\n%q", conn.writeBuf.Bytes()) }
<-done
は、ハンドラが完了するまでメインゴルーチンをブロックします。ハンドラが完了すると、testConn
のwriteBuf
にサーバーが書き込んだレスポンス全体が格納されています。bytes.HasSuffix
を使用して、conn.writeBuf.Bytes()
の末尾が期待されるチャンク形式 (\r\n\r\n3\r\nxyz\r\n0\r\n\r\n
) で終わっているかを確認します。\r\n\r\n
: HTTPヘッダーの終わりを示す空行。3
: 最初のチャンクのサイズ(16進数で3バイト)。\r\n
: チャンクサイズとデータの区切り。xyz
: チャンクデータ。\r\n
: チャンクデータと次のチャンクサイズ(または終端チャンク)の区切り。0
: 終端チャンクのサイズ(0バイト)。\r\n
: 終端チャンクサイズとトレーラーヘッダー(なし)の区切り。\r\n
: 最終的なメッセージの終わり。
もし期待される形式でなければ、
t.Errorf
が呼び出され、テストが失敗したことを報告し、実際のレスポンス内容を表示します。
このテストは、net/http
パッケージのチャンク転送エンコーディングの実装が、小さな書き込みを効率的にバッファリングして単一のチャンクとして送信するという、望ましい動作をしているかどうかを検証するためのものです。
関連リンク
- Go Issue 2357: https://github.com/golang/go/issues/2357
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
bytes
パッケージドキュメント: https://pkg.go.dev/bytes