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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるHTTPレスポンスの Content-Length ヘッダの挙動に関する修正です。具体的には、HTTPハンドラがクライアントに何も書き込まなかった場合に、明示的に Content-Length: 0 ヘッダをレスポンスに追加するように変更されています。これにより、特にHTTP/1.0のKeep-Alive接続の挙動が改善され、HTTP/1.1においてもより明確なレスポンスが保証されます。

コミット

commit 49f29c9c22f15120811719f4f451c3469e9e174a
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sun Aug 26 11:17:55 2012 -0700

    net/http: send an explicit zero Content-Length when Handler never Writes
    
    Fixes #4004
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6472055

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

https://github.com/golang/go/commit/49f29c9c22f15120811719f4f451c3469e9e174a

元コミット内容

このコミットの目的は、net/http パッケージにおいて、HTTPハンドラがレスポンスボディに何も書き込まなかった場合に、HTTPレスポンスに明示的に Content-Length: 0 ヘッダを付与することです。これは、GoのIssue #4004で報告された問題への対応です。

変更の背景

HTTPプロトコルにおいて、レスポンスボディが存在しない場合、サーバーは Content-Length: 0 ヘッダを送信するか、またはチャンク転送エンコーディング(Transfer-Encoding: chunked)を使用してボディが空であることを示すのが一般的です。

このコミット以前の net/http パッケージでは、ハンドラが ResponseWriter に何も書き込まなかった場合、Content-Length ヘッダが設定されないことがありました。この挙動は、特にHTTP/1.0のKeep-Alive接続において問題を引き起こす可能性がありました。

HTTP/1.0のKeep-Alive接続では、次のリクエストを同じTCP接続で送信するために、現在のレスポンスの終わりを正確に知る必要があります。Content-Length ヘッダがない場合、クライアントはレスポンスの終わりを判断できず、接続がハングアップしたり、タイムアウトしたりする可能性がありました。これは、クライアントがサーバーからのデータ受信を無限に待機してしまうためです。

HTTP/1.1では、Content-Length がない場合はチャンク転送エンコーディングがデフォルトで適用されるため、この問題はHTTP/1.0ほど深刻ではありませんが、明示的な Content-Length: 0 はより明確で効率的な通信を可能にします。

Issue #4004では、この Content-Length ヘッダの欠如が原因で、一部のHTTPクライアント(特にHTTP/1.0 Keep-Aliveを使用するクライアント)が正しく動作しないという報告がなされていました。このコミットは、この問題を解決し、より堅牢なHTTPサーバーの挙動を保証するために導入されました。

前提知識の解説

HTTP/1.0とHTTP/1.1

  • HTTP/1.0: 各リクエスト/レスポンスのペアごとに新しいTCP接続を確立するのが基本でした。Connection: Keep-Alive ヘッダを使用することで、複数のリクエスト/レスポンスを単一のTCP接続で処理する「Keep-Alive」機能が導入されましたが、これはオプションであり、その実装には注意が必要でした。レスポンスボディの長さを知るためには、通常 Content-Length ヘッダが必須でした。
  • HTTP/1.1: 持続的接続(Persistent Connections)がデフォルトとなり、Connection: Keep-Alive ヘッダは不要になりました。これにより、TCP接続のオーバーヘッドが削減され、パフォーマンスが向上しました。HTTP/1.1では、Content-Length ヘッダがない場合、またはボディの長さが事前に不明な場合に Transfer-Encoding: chunked を使用してボディを送信するメカニズムが導入されました。

Content-Length ヘッダ

HTTPレスポンスのボディのバイト数を指定するヘッダです。このヘッダが存在する場合、クライアントは指定されたバイト数を受信した時点でレスポンスボディの終わりを判断できます。

Transfer-Encoding: chunked

レスポンスボディの長さが事前に不明な場合や、動的に生成される場合に用いられる転送エンコーディングです。ボディは複数の「チャンク」に分割され、各チャンクの前にそのチャンクのサイズが記述されます。ボディの終わりは、サイズが0のチャンク(EOFチャンク)によって示されます。

Go net/http パッケージ

Go言語の標準ライブラリに含まれるHTTPクライアントおよびサーバーの実装を提供するパッケージです。http.Handler インターフェースを実装することで、カスタムのHTTPハンドラを作成できます。http.ResponseWriter は、HTTPレスポンスをクライアントに書き込むためのインターフェースです。

Keep-Alive接続

単一のTCP接続を再利用して複数のHTTPリクエスト/レスポンスを処理するメカニズムです。これにより、TCP接続の確立と切断にかかるオーバーヘッドが削減され、Webページのロード時間が短縮されます。

技術的詳細

この修正は、net/http パッケージの response 構造体の finishRequest メソッドに焦点を当てています。このメソッドは、HTTPハンドラがリクエストの処理を完了した後に呼び出され、最終的なレスポンスヘッダの調整や接続の管理を行います。

変更前は、ハンドラが ResponseWriter に何も書き込まなかった場合(w.written == 0)、かつ Content-Length ヘッダが明示的に設定されていなかった場合、Content-Length ヘッダはレスポンスに含まれませんでした。

変更後、finishRequest メソッドに以下のロジックが追加されました。

	// If the handler never wrote any bytes and never sent a Content-Length
	// response header, set the length explicitly to zero. This helps
	// HTTP/1.0 clients keep their "keep-alive" connections alive, and for
	// HTTP/1.1 clients is just as good as the alternative: sending a
	// chunked response and immediately sending the zero-length EOF chunk.
	if w.written == 0 && w.header.get("Content-Length") == "" {
		w.header.Set("Content-Length", "0")
	}

このコードは、以下の条件が両方とも真である場合に実行されます。

  1. w.written == 0: ハンドラがレスポンスボディに1バイトも書き込まなかった場合。
  2. w.header.get("Content-Length") == "": ハンドラが Content-Length ヘッダを明示的に設定しなかった場合。

これらの条件が満たされた場合、w.header.Set("Content-Length", "0") が呼び出され、レスポンスヘッダに Content-Length: 0 が追加されます。

この変更の利点は以下の通りです。

  • HTTP/1.0 Keep-Aliveの改善: クライアントがレスポンスボディの終わりを正確に判断できるようになり、接続のハングアップやタイムアウトを防ぎます。
  • HTTP/1.1の明確化: Content-Length: 0 は、ボディが空であることを明確に示します。これは、チャンク転送で0バイトのEOFチャンクを送信するのと同等であり、よりシンプルで理解しやすいです。
  • 堅牢性の向上: さまざまなHTTPクライアントやプロキシとの互換性が向上し、予期せぬ挙動を防ぎます。

また、この変更を検証するために、src/pkg/net/http/serve_test.goTestContentLengthZero という新しいテストケースが追加されました。このテストは、HTTP/1.0とHTTP/1.1の両方で、ハンドラが何も書き込まない場合に Content-Length: 0 が正しく設定されることを確認します。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -512,8 +512,16 @@ func (w *response) Write(data []byte) (n int, err error) {
 }
 
 func (w *response) finishRequest() {
-\t// If this was an HTTP/1.0 request with keep-alive and we sent a Content-Length
-\t// back, we can make this a keep-alive response ...
+\t// If the handler never wrote any bytes and never sent a Content-Length
+\t// response header, set the length explicitly to zero. This helps
+\t// HTTP/1.0 clients keep their "keep-alive" connections alive, and for
+\t// HTTP/1.1 clients is just as good as the alternative: sending a
+\t// chunked response and immediately sending the zero-length EOF chunk.
+\tif w.written == 0 && w.header.get("Content-Length") == "" {
+\t\tw.header.Set("Content-Length", "0")
+\t}
+\t// If this was an HTTP/1.0 request with keep-alive and we sent a
+\t// Content-Length back, we can make this a keep-alive response ...
 \tif w.req.wantsHttp10KeepAlive() {
 \t\tsentLength := w.header.get("Content-Length") != ""
 \t\tif sentLength && w.header.get("Connection") == "keep-alive" {

src/pkg/net/http/serve_test.go

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1207,6 +1207,38 @@ func TestCaseSensitiveMethod(t *testing.T) {
 	res.Body.Close()
 }
 
+// TestContentLengthZero tests that for both an HTTP/1.0 and HTTP/1.1
+// request (both keep-alive), when a Handler never writes any
+// response, the net/http package adds a "Content-Length: 0" response
+// header.
+func TestContentLengthZero(t *testing.T) {
+	ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {}))\n
+	defer ts.Close()\n
+\n
+	for _, version := range []string{"HTTP/1.0", "HTTP/1.1"} {\n
+		conn, err := net.Dial("tcp", ts.Listener.Addr().String())\n
+		if err != nil {\n
+			t.Fatalf("error dialing: %v", err)\n
+		}\n
+		_, err = fmt.Fprintf(conn, "GET / %v\\r\\nConnection: keep-alive\\r\\nHost: foo\\r\\n\\r\\n", version)\n
+		if err != nil {\n
+			t.Fatalf("error writing: %v", err)\n
+		}\n
+		req, _ := NewRequest("GET", "/", nil)\n
+		res, err := ReadResponse(bufio.NewReader(conn), req)\n
+		if err != nil {\n
+			t.Fatalf("error reading response: %v", err)\n
+		}\n
+		if te := res.TransferEncoding; len(te) > 0 {\n
+			t.Errorf("For version %q, Transfer-Encoding = %q; want none", version, te)\n
+		}\n
+		if cl := res.ContentLength; cl != 0 {\n
+			t.Errorf("For version %q, Content-Length = %v; want 0", version, cl)\n
+		}\n
+		conn.Close()\n
+	}\n
+}\n
+
 // goTimeout runs f, failing t if f takes more than ns to complete.\n func goTimeout(t *testing.T, d time.Duration, f func()) {\n 	ch := make(chan bool, 2)\n```

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

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

`response` 構造体の `finishRequest()` メソッドは、HTTPリクエストの処理が完了した際に、レスポンスの最終的な状態を確定させる役割を担います。
追加された `if` ブロックは、以下の条件をチェックします。

*   `w.written == 0`: これは、`response` オブジェクトが管理する内部カウンタで、ハンドラが `ResponseWriter` に書き込んだバイト数を示します。これが `0` であるということは、ハンドラがレスポンスボディに何もデータを書き込まなかったことを意味します。
*   `w.header.get("Content-Length") == ""`: これは、ハンドラが `Content-Length` ヘッダを明示的に設定しなかったことを意味します。`get` メソッドは、ヘッダが存在しない場合に空文字列を返します。

これらの条件が両方とも真である場合、つまり、ハンドラが何も書き込まず、かつ `Content-Length` ヘッダも設定しなかった場合にのみ、`w.header.Set("Content-Length", "0")` が実行されます。これにより、レスポンスに `Content-Length: 0` が追加され、ボディが空であることを明示的に示します。

この変更は、特にHTTP/1.0のKeep-Alive接続において重要です。HTTP/1.0クライアントは、`Content-Length` ヘッダがないと、レスポンスの終わりを判断できず、接続がハングアップする可能性があります。`Content-Length: 0` を明示的に送信することで、クライアントはボディが空であることを認識し、次のリクエストのために接続を再利用できます。

HTTP/1.1の場合でも、この変更は有効です。HTTP/1.1では `Content-Length` がない場合、チャンク転送がデフォルトで適用されますが、`Content-Length: 0` を送信することは、0バイトのEOFチャンクを即座に送信するのと同等であり、よりシンプルで効率的な方法です。

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

`TestContentLengthZero` という新しいテスト関数が追加されました。このテストは、以下の手順で `Content-Length: 0` の挙動を検証します。

1.  `httptest.NewServer` を使用してテスト用のHTTPサーバーを起動します。このサーバーのハンドラは、`HandlerFunc(func(rw ResponseWriter, req *Request) {})` のように、何も書き込まない空の関数です。
2.  HTTP/1.0とHTTP/1.1の両方のバージョンに対してループを実行します。
3.  各バージョンで、`net.Dial` を使用してサーバーへのTCP接続を確立します。
4.  `fmt.Fprintf` を使用して、`GET /` リクエストと `Connection: keep-alive` ヘッダを含むHTTPリクエストを手動で送信します。これにより、ハンドラが何も書き込まない状況をシミュレートします。
5.  `ReadResponse` を使用してサーバーからのレスポンスを読み取ります。
6.  レスポンスの `TransferEncoding` が設定されていないこと(`len(te) > 0` でないこと)を確認します。これは、`Content-Length` が設定されている場合にチャンク転送が使用されないことを保証するためです。
7.  レスポンスの `ContentLength` が `0` であることを確認します。これがこのテストの主要なアサーションであり、ハンドラが何も書き込まなかった場合に `Content-Length: 0` が正しく追加されたことを検証します。
8.  TCP接続を閉じます。

このテストは、このコミットによって導入された `Content-Length: 0` の自動付与ロジックが、意図した通りに機能していることを保証します。

## 関連リンク

*   Go Issue #4004: [https://github.com/golang/go/issues/4004](https://github.com/golang/go/issues/4004)
*   Go CL 6472055: [https://golang.org/cl/6472055](https://golang.org/cl/6472055)

## 参考にした情報源リンク

*   HTTP/1.0 と HTTP/1.1 の違い: [https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP)
*   HTTP Content-Length ヘッダ: [https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Length](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Length)
*   HTTP Transfer-Encoding ヘッダ: [https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Transfer-Encoding](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Transfer-Encoding)
*   Go `net/http` パッケージドキュメント: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
*   Go `httptest` パッケージドキュメント: [https://pkg.go.dev/net/http/httptest](https://pkg.go.dev/net/http/httptest)
*   Go `net` パッケージドキュメント: [https://pkg.go.dev/net](https://pkg.go.dev/net)
*   Go `fmt` パッケージドキュメント: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
*   Go `bufio` パッケージドキュメント: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
*   Go `testing` パッケージドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
*   Go `time` パッケージドキュメント: [https://pkg.go.dev/time](https://pkg.go.dev/time)