[インデックス 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")
}
このコードは、以下の条件が両方とも真である場合に実行されます。
w.written == 0
: ハンドラがレスポンスボディに1バイトも書き込まなかった場合。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.go
に TestContentLengthZero
という新しいテストケースが追加されました。このテストは、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)