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

[インデックス 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 実装です。実際のネットワーク接続の代わりに、メモリ上のバッファ(readBufwriteBuf)を使用してデータの読み書きをシミュレートします。これにより、ネットワークI/Oを伴わずにHTTPサーバーの動作をテストできます。
  • oneConnListener: net.Listener インターフェースのカスタム実装で、一度だけ接続(testConn)を受け入れるように設計されています。これにより、単一のHTTPリクエスト/レスポンスサイクルをテストできます。
  • bytes.HasSuffix(s, suffix []byte) bool: bytes パッケージの関数で、バイトスライス s がバイトスライス suffix で終わるかどうかをチェックします。テストでは、レスポンスボディが期待されるチャンク形式で終わっているかを確認するために使用されます。

技術的詳細

追加された TestServerBufferedChunking テストは、net/http サーバーがチャンク転送エンコーディングを使用する際のバッファリング動作を検証することを目的としています。

  1. テストの無効化:

    if true {
        t.Logf("Skipping known broken test; see Issue 2357")
        return
    }
    

    この if true ブロックにより、テストは常にスキップされます。これは、テストが当時の net/http の実装では失敗することが分かっていたためです。この記述は、バグが修正された際にこのブロックを削除し、テストを有効化するためのプレースホルダーとして機能します。

  2. 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)を書き込みます。これは、サーバーが読み取るクライアントからのリクエストをシミュレートします。

  3. oneConnListener のセットアップ:

    done := make(chan bool)
    ls := &oneConnListener{conn}
    

    テストの完了を通知するためのチャネル done と、testConn をラップする oneConnListener を作成します。

  4. 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バイトのデータをレスポンスボディに書き込みます。
  5. テスト結果の検証:

    <-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 チャネルからの受信を待ち、ハンドラの完了を待ちます。その後、testConnwriteBuf に書き込まれたサーバーのレスポンスを検証します。

    • 期待される結果は、\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 を受け取ります。

  1. テストのスキップ:

    if true {
        t.Logf("Skipping known broken test; see Issue 2357")
        return
    }
    

    このブロックは、テストが実行されるとすぐに t.Logf を使ってログメッセージを出力し、return でテスト関数を終了させます。これにより、テストは実行されずにスキップされます。これは、このテストが当時の net/http の実装では失敗することが分かっていたため、CI/CDパイプラインなどで常に失敗するテストとして残しておくのではなく、一時的に無効化するための一般的なパターンです。Issue 2357が解決されたら、この if true を削除してテストを有効化することが意図されています。

  2. テスト接続の準備:

    conn := new(testConn)
    conn.readBuf.Write([]byte("GET / HTTP/1.1\\r\\n\\r\\n"))
    

    testConnnet.Conn インターフェースを実装するテスト用の構造体で、実際のTCP接続の代わりにメモリ上のバッファ (readBufwriteBuf) を使用します。ここでは、クライアントからのリクエストをシミュレートするために、GET / HTTP/1.1\r\n\r\n というHTTPリクエストを conn.readBuf に書き込んでいます。

  3. リスナーと完了チャネルの準備:

    done := make(chan bool)
    ls := &oneConnListener{conn}
    

    done チャネルは、HTTPハンドラが処理を完了したことをメインのテストゴルーチンに通知するために使用されます。oneConnListenernet.Listener インターフェースを実装するテスト用の構造体で、一度だけ conn を返すように設定されています。これにより、単一のHTTPリクエスト/レスポンスサイクルをテストできます。

  4. 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"): ResponseWriterHeader() メソッドを使ってレスポンスヘッダーを取得し、Content-Typetext/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 のように単一のチャンクとして送信されることを確認することです。
  5. 結果の検証:

    <-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 は、ハンドラが完了するまでメインゴルーチンをブロックします。ハンドラが完了すると、testConnwriteBuf にサーバーが書き込んだレスポンス全体が格納されています。 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 パッケージのチャンク転送エンコーディングの実装が、小さな書き込みを効率的にバッファリングして単一のチャンクとして送信するという、望ましい動作をしているかどうかを検証するためのものです。

関連リンク

参考にした情報源リンク