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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージにおける、HTTPレスポンスのContent-Lengthヘッダーが二重に送信されるバグを修正します。具体的には、response.goresponsewrite_test.gotransfer.goの3つのファイルが変更されています。

コミット

commit 1e6a19be641b348547563b762b51d2b62de12da4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Jun 10 16:52:37 2014 -0700

    net/http: fix double Content-Length in response
    
    Fixes #8180
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/105040043

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

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

元コミット内容

net/http: レスポンスにおける二重のContent-Lengthヘッダーを修正

Issue #8180を修正

変更の背景

HTTP/1.1プロトコルでは、メッセージボディの長さをクライアントに伝えるためにContent-Lengthヘッダーを使用します。このヘッダーは、メッセージボディがどれだけのバイト数であるかを示す単一の値を持つべきです。しかし、特定の条件下でnet/httpパッケージが生成するHTTPレスポンスに、このContent-Lengthヘッダーが二重に含まれてしまうバグが存在しました。

HTTPプロトコルにおいて、同じヘッダーが複数回出現する場合、その解釈はヘッダーの種類によって異なります。Content-Lengthヘッダーの場合、RFC 7230のセクション3.3.2「Content-Length」では、「受信者は、複数のContent-Lengthヘッダーフィールドを受信した場合、メッセージを不正なものとして拒否するか、またはすべてのContent-Lengthフィールド値が同一である場合にのみ、そのメッセージを処理しなければならない」と規定されています。つまり、二重のContent-Lengthヘッダーは、HTTPクライアントやプロキシがレスポンスを正しく処理できない原因となり、互換性の問題や予期せぬ動作を引き起こす可能性があります。

このバグは、特にPOSTPUTリクエストに対するレスポンスで、ボディが空(Content-Length: 0)である場合に顕著に発生していました。Goのnet/httpパッケージの内部ロジックが、特定の条件下で既にContent-Lengthヘッダーが設定されているにもかかわらず、再度Content-Length: 0を書き込んでしまうことが原因でした。

前提知識の解説

HTTP Content-Length ヘッダー

Content-Lengthヘッダーは、HTTPメッセージ(リクエストまたはレスポンス)のボディのオクテット(バイト)単位の長さを指定するために使用されます。これは、受信側がメッセージボディの終わりを正確に判断し、接続を再利用したり、次のリクエストを送信したりするために不可欠です。

HTTP Transfer-Encoding: chunked

Transfer-Encoding: chunkedは、メッセージボディの長さを事前に知ることができない場合(例えば、動的に生成されるコンテンツ)に使用される転送エンコーディングです。この場合、メッセージボディは「チャンク」と呼ばれる小さな断片に分割され、各チャンクの前にそのサイズが記述されます。Transfer-Encoding: chunkedが使用される場合、Content-Lengthヘッダーは送信されません。両方が存在するとプロトコル違反となります。

Go net/http パッケージ

net/httpパッケージは、Go言語でHTTPクライアントとサーバーを実装するための標準ライブラリです。このパッケージは、HTTPリクエストの解析、レスポンスの生成、ヘッダーの処理、ボディの読み書きなど、HTTP通信の低レベルな詳細を抽象化し、開発者が簡単にWebアプリケーションを構築できるようにします。

HTTPメソッドとボディの有無

HTTPメソッドには、GETPOSTPUTDELETEなどがあります。GETリクエストは通常ボディを持ちませんが、POSTPUTリクエストは通常、サーバーにデータを送信するためにリクエストボディを持ちます。レスポンスにおいても、GETリクエストに対する成功レスポンスはボディを持つことが多いですが、POSTPUTに対する成功レスポンス(例: 200 OK, 204 No Content)はボディを持たないか、非常に短いボディを持つことがあります。

技術的詳細

このコミットが修正する問題は、net/httpパッケージがHTTPレスポンスを書き込む際に、Content-Length: 0ヘッダーが不適切に二重に書き込まれる可能性があった点です。

従来のResponse.Writeメソッドのロジックでは、レスポンスのContentLengthが0であり、かつTransfer-Encodingがチャンク形式でない場合に、無条件にContent-Length: 0ヘッダーを書き込んでいました。

// 修正前
if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) {
    if _, err := io.WriteString(w, "Content-Length: 0\r\n"); err != nil {
        return err
    }
}

しかし、transferWriterの内部ロジック(shouldSendContentLength())によって、既にContent-Lengthヘッダーが書き込まれている場合がありました。特に、POSTPUTリクエストに対するレスポンスで、ボディが空であるにもかかわらず、transferWriterContent-Lengthヘッダーを送信すべきだと判断した場合に、この二重書き込みが発生していました。これは、transferWriterがリクエストメソッドに基づいてContent-Lengthを送信するかどうかを決定するロジックを持っていたためです。

このコミットでは、この問題を解決するために、Response.Writeメソッド内でContent-Length: 0を書き込む条件に、contentLengthAlreadySentという新しいフラグを追加しました。このフラグは、transferWriterが既にContent-Lengthヘッダーを送信したかどうかを示します。

// 修正後
contentLengthAlreadySent := tw.shouldSendContentLength() // twはtransferWriterのインスタンス
if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) && !contentLengthAlreadySent {
    if _, err := io.WriteString(w, "Content-Length: 0\r\n"); err != nil {
        return err
    }
}

これにより、Content-Lengthが0で、チャンクエンコーディングが使用されておらず、かつまだContent-Lengthヘッダーが送信されていない場合のみContent-Length: 0が書き込まれるようになります。

また、transfer.gotransferWriter.WriteHeaderメソッド内でも、Content-Lengthヘッダーを書き込む際にエラーハンドリングが追加されました。これは、io.WriteStringがエラーを返す可能性があるため、より堅牢なコードにするための変更です。

テストケースも追加され、POSTリクエストに対する空のレスポンスでContent-Lengthヘッダーが一つだけ送信されることを確認しています。

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

src/pkg/net/http/response.go

  • Response.Writeメソッド内のContent-Length: 0を書き込む条件に、contentLengthAlreadySentという新しい条件が追加されました。
  • contentLengthAlreadySentは、transferWritershouldSendContentLength()メソッドの呼び出し結果によって設定されます。
--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -266,7 +266,10 @@ func (r *Response) Write(w io.Writer) error {
 		return err
 	}
 
-	if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) {
+	// contentLengthAlreadySent may have been already sent for
+	// POST/PUT requests, even if zero length. See Issue 8180.
+	contentLengthAlreadySent := tw.shouldSendContentLength()
+	if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) && !contentLengthAlreadySent {
 		if _, err := io.WriteString(w, "Content-Length: 0\r\n"); err != nil {
 			return err
 		}

src/pkg/net/http/responsewrite_test.go

  • TestResponseWrite関数に新しいテストケースが追加されました。
  • このテストケースは、POSTリクエストに対するContent-Length: 0のレスポンスが、単一のContent-Lengthヘッダーを持つことを検証します。
--- a/src/pkg/net/http/responsewrite_test.go
+++ b/src/pkg/net/http/responsewrite_test.go
@@ -191,6 +191,22 @@ func TestResponseWrite(t *testing.T) {
 				"Foo: Bar Baz\r\n" +\
 				"\r\n",
 		},
+
+		// Want a single Content-Length header. Fixing issue 8180 where
+		// there were two.
+		{
+			Response{
+				StatusCode:       StatusOK,
+				ProtoMajor:       1,
+				ProtoMinor:       1,
+				Request:          &Request{Method: "POST"},
+				Header:           Header{},
+				ContentLength:    0,
+				TransferEncoding: nil,
+				Body:             nil,
+			},
+			"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
+		},
 	}
 
 	for i := range respWriteTests {

src/pkg/net/http/transfer.go

  • transferWriter.WriteHeaderメソッド内で、Content-Lengthヘッダーを書き込むio.WriteString呼び出しにエラーハンドリングが追加されました。
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -155,7 +155,9 @@ func (t *transferWriter) WriteHeader(w io.Writer) error {
 	// function of the sanitized field triple (Body, ContentLength,
 	// TransferEncoding)
 	if t.shouldSendContentLength() {
-		io.WriteString(w, "Content-Length: ")
+		if _, err := io.WriteString(w, "Content-Length: "); err != nil {
+			return err
+		}
 		if _, err := io.WriteString(w, strconv.FormatInt(t.ContentLength, 10)+"\r\n"); err != nil {
 			return err
 		}

コアとなるコードの解説

src/pkg/net/http/response.go の変更

この変更の核心は、Response.WriteメソッドにおけるContent-Length: 0の書き込みロジックの改善です。

  • contentLengthAlreadySent := tw.shouldSendContentLength(): ここで、transferWritertw)が既にContent-Lengthヘッダーを送信するべきかどうかを判断します。transferWriterは、リクエストメソッド(例: POST)やレスポンスボディの有無などに基づいて、Content-Lengthヘッダーの送信が必要かどうかを決定する内部ロジックを持っています。
  • if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) && !contentLengthAlreadySent: この新しい条件式が、二重のContent-Lengthヘッダーを防ぐ鍵となります。
    • r1.ContentLength == 0: レスポンスボディの長さが0であること。
    • !chunked(r1.TransferEncoding): Transfer-Encodingがチャンク形式でないこと。チャンク形式の場合、Content-Lengthは不要です。
    • !contentLengthAlreadySent: これが追加された最も重要な条件です。 transferWriterがまだContent-Lengthヘッダーを送信していない場合にのみ、Content-Length: 0を書き込みます。これにより、transferWriterが既にContent-Lengthヘッダーを処理している場合でも、Response.Writeが重複してContent-Length: 0を書き込むことを防ぎます。

この変更により、POSTPUTリクエストに対する空のレスポンスなど、特定のシナリオで発生していたContent-Lengthヘッダーの二重送信が解消されます。

src/pkg/net/http/responsewrite_test.go の変更

追加されたテストケースは、この修正が意図通りに機能することを確認するためのものです。

{
    Response{
        StatusCode:       StatusOK,
        ProtoMajor:       1,
        ProtoMinor:       1,
        Request:          &Request{Method: "POST"}, // POSTリクエストをシミュレート
        Header:           Header{},
        ContentLength:    0, // Content-Lengthが0
        TransferEncoding: nil,
        Body:             nil,
    },
    "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", // 期待されるレスポンス(Content-Lengthが一つだけ)
},

このテストは、POSTメソッドのリクエストに対するレスポンスで、Content-Lengthが0の場合に、期待される出力がContent-Length: 0が一度だけ含まれることを検証します。これにより、以前のバグ(二重のContent-Length)が修正されたことを確認できます。

src/pkg/net/http/transfer.go の変更

transferWriter.WriteHeader内の変更は、主に堅牢性の向上を目的としています。

if _, err := io.WriteString(w, "Content-Length: "); err != nil {
    return err
}

io.WriteStringは、書き込み操作中にエラーが発生する可能性があります。以前のコードでは、このエラーが適切に処理されていませんでした。この変更により、Content-Length: という文字列を書き込む際にエラーが発生した場合、そのエラーが即座に返されるようになり、潜在的なデータ破損や予期せぬ動作を防ぎます。これは、バグ修正というよりは、コードの品質と信頼性を高めるための改善です。

関連リンク

参考にした情報源リンク

  • RFC 7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing - Section 3.3.2. Content-Length: https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
  • Go net/http package documentation: https://pkg.go.dev/net/http
  • (注: コミットメッセージに記載されているIssue #8180は、公開されているGoのIssueトラッカーでは直接見つけることができませんでした。これは、内部的なトラッキング番号であるか、非常に古い、現在はアーカイブされたIssueである可能性があります。)