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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるHTTPサーバーのコンテンツタイプスニッフィング機能に存在したバグを修正し、それに関連するテストを追加するものです。具体的には、レスポンスボディの初期部分をバッファリングする際の変数スコープの問題が原因で発生していた「ショートライト(short writes)」、つまり期待されるバイト数よりも少ないバイト数しか書き込まれない現象を解決しています。

コミット

commit 1e85f41fd512d570b04f87d906d44e456f1c2108
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Nov 28 11:51:34 2011 -0500

    http: fix sniffing bug causing short writes
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5442045

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

https://github.com/golang/go/commit/1e85f41fd512d570b04f87d906d44e456f1c2108

元コミット内容

このコミットの目的は、HTTPサーバーがレスポンスのコンテンツタイプを自動判別(スニッフィング)する際に発生していたバグを修正し、その結果として生じていた「ショートライト」の問題を解決することです。

変更の背景

Goの net/http パッケージのHTTPサーバーは、クライアントにレスポンスを送信する際、Content-Type ヘッダが明示的に設定されていない場合、レスポンスボディの最初の数バイトを検査して適切なコンテンツタイプを推測(スニッフィング)します。このスニッフィング処理のために、サーバーはレスポンスボディの初期データを内部バッファに一時的に蓄積します。

問題は、このバッファリング処理において、利用可能なバッファ容量を計算する変数 m の宣言方法にありました。元のコードでは m := cap(w.conn.body) - len(w.conn.body):= (ショート変数宣言) を使用していました。これにより、w.conn.body のスコープ外で既に m という変数が存在していた場合でも、この行で新しいローカル変数 m が宣言されてしまい、外側のスコープの m とは異なる値を持つことになります。

この誤ったスコープの m が、Write メソッドが実際に書き込むべきバイト数を誤って計算する原因となり、結果として io.WriteString のような関数が期待されるバイト数よりも少ないバイト数しか書き込まない「ショートライト」という現象を引き起こしていました。これは、特に大きなデータを送信する際に、データが途中で切り詰められたり、複数回に分けて送信されることでパフォーマンスが低下したり、アプリケーションのロジックが期待通りに動作しない可能性がありました。

このバグは、HTTPレスポンスの完全性と信頼性に影響を与えるため、修正が必要とされました。

前提知識の解説

1. Go言語の変数宣言とスコープ (:==)

Go言語には、変数を宣言し初期化する方法がいくつかあります。

  • var name type = value: 明示的な型指定と初期化。
  • var name type: 型のみ指定し、ゼロ値で初期化。
  • name := value: ショート変数宣言。関数内で新しい変数を宣言し、初期化する際に使用します。型はGoコンパイラが自動的に推論します。重要なのは、:= は常に新しい変数を宣言するという点です。もし同じ名前の変数が既に現在のスコープに存在する場合、:= を使うと新しいローカル変数が宣言され、外側の変数を「シャドーイング(shadowing)」します。
  • name = value: 既に宣言されている変数に値を代入する場合に使用します。この場合、新しい変数は宣言されず、既存の変数の値が更新されます。

このコミットのバグは、:= を使用したことで、既存の m 変数ではなく新しい m 変数が作成され、意図しない動作を引き起こしたことに起因します。

2. HTTPコンテンツタイプスニッフィング

Webサーバーは、クライアント(ブラウザなど)にレスポンスを返す際、Content-Type HTTPヘッダを付けて、レスポンスボディのメディアタイプ(例: text/html, application/json, image/png)を通知します。これにより、ブラウザはコンテンツを適切にレンダリングできます。

しかし、アプリケーションが Content-Type ヘッダを明示的に設定しない場合もあります。このような場合、多くのHTTPサーバーやブラウザは、レスポンスボディの最初の数バイトを読み取り、その内容からコンテンツタイプを「推測(sniff)」しようとします。このプロセスを「コンテンツタイプスニッフィング」と呼びます。

Goの net/http パッケージのサーバーもこの機能を持っており、http.ResponseWriterWrite メソッドが最初に呼び出された際に、内部バッファにデータを蓄積し、そのデータに基づいてコンテンツタイプを決定します。w.conn.body はこの内部バッファを指していると考えられます。

3. Goのスライスと容量 (cap()len())

Goのスライスは、配列への参照のようなものです。スライスには以下の2つの重要なプロパティがあります。

  • len(s): スライス s の現在の要素数(長さ)。
  • cap(s): スライス s が参照している基底配列の容量。スライスが拡張されることなく保持できる最大要素数。

cap(w.conn.body) - len(w.conn.body) は、w.conn.body スライスに現在残っている、追加でデータを格納できる空き容量を計算しています。この計算結果が、Write メソッドがバッファに書き込める最大バイト数となります。

4. io.WriteString と「ショートライト」

io.WriteString は、指定された文字列を io.Writer インターフェースを実装するオブジェクトに書き込むためのヘルパー関数です。この関数は、書き込まれたバイト数とエラーを返します。

「ショートライト」とは、Write 操作が要求されたバイト数よりも少ないバイト数しか書き込まなかった場合に発生する現象です。これは、バッファが満杯になった、I/Oエラーが発生した、または今回のケースのように、書き込み可能な容量の計算が誤っていた場合などに起こり得ます。HTTPレスポンスの文脈では、ショートライトはデータが途中で切り詰められたり、不完全なレスポンスが送信されたりする原因となります。

技術的詳細

このコミットの核となる修正は、src/pkg/net/http/server.go ファイルの (*response).Write メソッド内の1行の変更です。

元のコード:

m := cap(w.conn.body) - len(w.conn.body)

修正後のコード:

m = cap(w.conn.body) - len(w.conn.body)

この変更は、m 変数の宣言方法を := (ショート変数宣言) から = (代入) に変更しています。

(*response).Write メソッドは、HTTPレスポンスボディへの書き込みを処理します。このメソッドの内部では、コンテンツタイプスニッフィングのために、レスポンスの初期部分を w.conn.body という内部バッファに蓄積します。

元のコードでは、m := cap(w.conn.body) - len(w.conn.body) と記述されていました。もし、この Write メソッドのより外側のスコープ(例えば、response 構造体自体や、Write メソッドの他の部分)に m という名前の変数が既に存在していた場合、:= を使用すると、この行で新しいローカル変数 m が宣言され、外側の m をシャドーイングしてしまいます。これにより、Write メソッドの残りの部分が、この新しく宣言されたローカルな m を参照することになり、意図しない動作を引き起こす可能性がありました。

具体的には、この mappend 操作がバッファを再割り当てしないように、書き込むデータの最大量を制限するために使用されていました。もし m が誤って計算されたり、期待されるスコープの m ではない新しい変数であった場合、Write メソッドは data スライスから m バイトしか書き込まず、残りのデータは書き込まれないままになる可能性がありました。これが「ショートライト」の原因です。

修正後の m = cap(w.conn.body) - len(w.conn.body) は、既存の m 変数に計算結果を代入します。これにより、変数のシャドーイングが回避され、Write メソッド全体で正しい m の値が使用されるようになります。結果として、バッファリングロジックが正しく機能し、期待されるすべてのデータが一度に書き込まれるようになります。

この修正は、Go言語における変数スコープとショート変数宣言の一般的な落とし穴の一つを浮き彫りにしています。特に、既存の変数に値を代入するつもりで誤って := を使用してしまうと、デバッグが困難なバグにつながることがあります。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -467,7 +467,7 @@ func (w *response) Write(data []byte) (n int, err error) {
 		// determine the content type.  Accumulate the
 		// initial writes in w.conn.body.
 		// Cap m so that append won't allocate.
-		m := cap(w.conn.body) - len(w.conn.body)
+		m = cap(w.conn.body) - len(w.conn.body)
 		if m > len(data) {
 			m = len(data)
 		}

src/pkg/net/http/sniff_test.go

--- a/src/pkg/net/http/sniff_test.go
+++ b/src/pkg/net/http/sniff_test.go
@@ -6,12 +6,14 @@ package http_test
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
 	. "net/http"
 	"net/http/httptest"
 	"strconv"
+	"strings"
 	"testing"
 )
 
@@ -112,3 +114,24 @@ func TestContentTypeWithCopy(t *testing.T) {
 	}\n\tresp.Body.Close()\n}\n+\n+func TestSniffWriteSize(t *testing.T) {\n+\tts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tsize, _ := strconv.Atoi(r.FormValue(\"size\"))\n+\t\twritten, err := io.WriteString(w, strings.Repeat(\"a\", size))\n+\t\tif err != nil {\n+\t\t\tt.Errorf(\"write of %d bytes: %v\", size, err)\n+\t\t\treturn\n+\t\t}\n+\t\tif written != size {\n+\t\t\tt.Errorf(\"write of %d bytes wrote %d bytes\", size, written)\n+\t\t}\n+\t}))\n+\tdefer ts.Close()\n+\tfor _, size := range []int{0, 1, 200, 600, 999, 1000, 1023, 1024, 512 << 10, 1 << 20} {\n+\t\t_, err := Get(fmt.Sprintf(\"%s/?size=%d\", ts.URL, size))\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"size %d: %v\", size, err)\n+\t\t}\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/net/http/server.go` の変更

この変更は、`response` 構造体の `Write` メソッド内で行われています。`Write` メソッドは、HTTPレスポンスボディにデータを書き込むための主要なインターフェースです。

```go
// determine the content type.  Accumulate the
// initial writes in w.conn.body.
// Cap m so that append won't allocate.
m = cap(w.conn.body) - len(w.conn.body) // 変更点: := から = へ
if m > len(data) {
    m = len(data)
}
  • コメントの意図: コメントは、このコードブロックがコンテンツタイプを決定するために w.conn.body に初期データを蓄積していることを説明しています。また、m を制限することで append が不要な再割り当て(allocation)を行わないようにしていることも示唆しています。
  • m = cap(w.conn.body) - len(w.conn.body): ここが修正の核心です。w.conn.body は、コンテンツタイプスニッフィングのために使用される内部バッファ(スライス)です。cap(w.conn.body) はそのバッファの総容量を、len(w.conn.body) は現在使用されている部分の長さを返します。したがって、cap(w.conn.body) - len(w.conn.body) は、バッファにまだ書き込める残りの空き容量を計算しています。
  • if m > len(data) { m = len(data) }: この行は、書き込もうとしているデータ data の長さが、バッファの残りの空き容量 m よりも小さい場合、実際に書き込むべきバイト数を len(data) に制限しています。これは、バッファの容量を超えて書き込もうとしないための安全策です。

この修正により、m は常に期待されるスコープの変数として扱われ、バッファの空き容量が正しく計算されるようになります。これにより、Write メソッドが data の全バイトを正しく処理し、ショートライトのバグが解消されます。

src/pkg/net/http/sniff_test.go の追加テスト

TestSniffWriteSize という新しいテスト関数が追加されました。このテストは、様々なサイズのレスポンスボディを送信し、期待されるバイト数が正確に書き込まれたことを検証することで、ショートライトのバグが修正されたことを確認します。

func TestSniffWriteSize(t *testing.T) {
	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
		size, _ := strconv.Atoi(r.FormValue("size"))
		written, err := io.WriteString(w, strings.Repeat("a", size))
		if err != nil {
			t.Errorf("write of %d bytes: %v", size, err)
			return
		}
		if written != size {
			t.Errorf("write of %d bytes wrote %d bytes", size, written)
		}
	}))
	defer ts.Close()
	for _, size := range []int{0, 1, 200, 600, 999, 1000, 1023, 1024, 512 << 10, 1 << 20} {
		_, err := Get(fmt.Sprintf("%s/?size=%d", ts.URL, size))
		if err != nil {
			t.Fatalf("size %d: %v", size, err)
		}
	}
}
  • httptest.NewServer: テスト用のHTTPサーバーを起動します。これにより、実際のネットワーク通信を伴うテストを簡単に実行できます。
  • HandlerFunc: サーバーがリクエストを受け取った際に実行されるハンドラ関数を定義します。
  • ハンドラ内のロジック:
    • size, _ := strconv.Atoi(r.FormValue("size")): リクエストのクエリパラメータ size から、書き込むべきバイト数を取得します。
    • written, err := io.WriteString(w, strings.Repeat("a", size)): io.WriteString を使用して、指定された size の 'a' 文字列をレスポンスライター w に書き込みます。strings.Repeat("a", size) は、指定された回数だけ 'a' を繰り返した文字列を生成します。
    • if err != nilif written != size: 書き込み中にエラーが発生しなかったか、そして実際に書き込まれたバイト数 written が期待される size と一致するかを検証します。一致しない場合はテストを失敗させます。
  • テストループ:
    • for _, size := range []int{...}: 0バイトから1MBまでの様々なサイズのデータをテストします。特に、コンテンツタイプスニッフィングのバッファサイズ(通常は512バイト)や、一般的なネットワークバッファサイズ(1KB、4KBなど)の境界値付近のサイズが含まれている点が重要です。
    • _, err := Get(fmt.Sprintf("%s/?size=%d", ts.URL, size)): テストサーバーに対してHTTP GETリクエストを送信し、指定された size のデータを要求します。
    • if err != nil: リクエスト中にエラーが発生した場合、テストを致命的に失敗させます。

このテストは、サーバーが様々なサイズのレスポンスを正確に書き込めることを保証し、以前のショートライトのバグが再発しないことを確認するための重要な追加です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • HTTPコンテンツタイプスニッフィングに関する一般的なWeb技術情報(例: MDN Web Docsなど)
  • Go言語のショート変数宣言とスコープに関する一般的なプログラミング記事