[インデックス 19509] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージにおける、HTTPレスポンスのContent-Length
ヘッダーが二重に送信されるバグを修正します。具体的には、response.go
、responsewrite_test.go
、transfer.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クライアントやプロキシがレスポンスを正しく処理できない原因となり、互換性の問題や予期せぬ動作を引き起こす可能性があります。
このバグは、特にPOST
やPUT
リクエストに対するレスポンスで、ボディが空(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メソッドには、GET
、POST
、PUT
、DELETE
などがあります。GET
リクエストは通常ボディを持ちませんが、POST
やPUT
リクエストは通常、サーバーにデータを送信するためにリクエストボディを持ちます。レスポンスにおいても、GET
リクエストに対する成功レスポンスはボディを持つことが多いですが、POST
やPUT
に対する成功レスポンス(例: 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
ヘッダーが書き込まれている場合がありました。特に、POST
やPUT
リクエストに対するレスポンスで、ボディが空であるにもかかわらず、transferWriter
がContent-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.go
のtransferWriter.WriteHeader
メソッド内でも、Content-Length
ヘッダーを書き込む際にエラーハンドリングが追加されました。これは、io.WriteString
がエラーを返す可能性があるため、より堅牢なコードにするための変更です。
テストケースも追加され、POST
リクエストに対する空のレスポンスでContent-Length
ヘッダーが一つだけ送信されることを確認しています。
コアとなるコードの変更箇所
src/pkg/net/http/response.go
Response.Write
メソッド内のContent-Length: 0
を書き込む条件に、contentLengthAlreadySent
という新しい条件が追加されました。contentLengthAlreadySent
は、transferWriter
のshouldSendContentLength()
メソッドの呼び出し結果によって設定されます。
--- 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()
: ここで、transferWriter
(tw
)が既に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
を書き込むことを防ぎます。
この変更により、POST
やPUT
リクエストに対する空のレスポンスなど、特定のシナリオで発生していた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:
という文字列を書き込む際にエラーが発生した場合、そのエラーが即座に返されるようになり、潜在的なデータ破損や予期せぬ動作を防ぎます。これは、バグ修正というよりは、コードの品質と信頼性を高めるための改善です。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1e6a19be641b348547563b762b51d2b62de12da4
- Go CL (Code Review): https://golang.org/cl/105040043
参考にした情報源リンク
- 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である可能性があります。)