[インデックス 19104] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内のHTTPレスポンスの書き出しに関する修正とテストの追加を行っています。具体的には、以下のファイルが変更されています。
src/pkg/net/http/response.go
:http.Response
構造体のWrite
メソッドの実装が修正されています。このメソッドは、http.Response
オブジェクトをio.Writer
に書き出す役割を担います。src/pkg/net/http/response_test.go
:http.Request
のダミー生成関数dummyReq11
が追加され、HTTP/1.1 リクエストのテストケースを容易に記述できるようになっています。src/pkg/net/http/responsewrite_test.go
:http.Response.Write
メソッドの様々なエッジケースをカバーするための、多数の新しいテストケースが追加されています。
コミット
commit a30eaa12eb2ecb484d3ced8775fadeeb20a21569
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Apr 10 17:12:31 2014 -0700
net/http: fix up Response.Write edge cases
The Go HTTP server doesn't use Response.Write, but others do,
so make it correct. Add a bunch more tests.
This bug is almost a year old. :/
Fixes #5381
LGTM=adg
R=golang-codereviews, adg
CC=dsymonds, golang-codereviews, rsc
https://golang.org/cl/85740046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a30eaa12eb2ecb484d3ced8775fadeeb20a21569
元コミット内容
このコミットは、Go言語の net/http
パッケージにおける Response.Write
メソッドのエッジケースを修正することを目的としています。コミットメッセージによると、GoのHTTPサーバー自体はこの Response.Write
メソッドを直接使用していませんが、他のコンポーネントやユーザーがこのメソッドを使用する可能性があるため、その正確性を確保する必要がありました。このバグは約1年前から存在していたと述べられており、#5381
というIssueを修正するものです。修正には、多数の追加テストが含まれています。
変更の背景
net/http
パッケージの Response.Write
メソッドは、http.Response
オブジェクトを io.Writer
に書き出すための汎用的な機能を提供します。GoのHTTPサーバーは通常、http.ResponseWriter
インターフェースを介してクライアントに応答を書き出すため、Response.Write
を直接使用することはありません。しかし、HTTPプロキシやカスタムHTTPクライアントなど、http.Response
オブジェクトを構築し、それを任意の io.Writer
に書き出す必要があるシナリオでは、このメソッドが利用されます。
このコミットが行われる前は、Response.Write
メソッドにいくつかのエッジケースにおけるバグが存在していました。特に、ContentLength
が不明 (-1
) な場合や、ボディが空であるかどうかの判断、そしてHTTP/1.1における Connection: close
ヘッダの適切な設定に関する問題があったと考えられます。これらの問題は、HTTPプロトコルの仕様に厳密に従わないレスポンスが生成される可能性があり、相互運用性の問題を引き起こす可能性がありました。コミットメッセージにある「This bug is almost a year old. :/」という記述から、この問題が長期間未解決であったことが伺えます。
この修正は、Response.Write
メソッドが、GoのHTTPサーバーが使用しない場合でも、HTTPプロトコルの仕様に準拠した正しいレスポンスを生成できるようにするために行われました。これにより、このメソッドを利用する他のアプリケーションやライブラリの信頼性が向上します。
前提知識の解説
このコミットの変更内容を理解するためには、以下のHTTPプロトコルおよびGo言語の基本的な概念を理解しておく必要があります。
HTTP/1.0 と HTTP/1.1
- HTTP/1.0: 各リクエスト/レスポンスのペアごとに新しいTCP接続を確立し、レスポンスの送信が完了すると接続を閉じることが一般的でした。
Content-Length
ヘッダは必須であり、レスポンスボディの長さを明示的に示す必要がありました。 - HTTP/1.1: 持続的接続(Persistent Connections)が導入され、単一のTCP接続上で複数のリクエスト/レスポンスをやり取りできるようになりました。これにより、接続の確立・切断のオーバーヘッドが削減され、パフォーマンスが向上しました。HTTP/1.1では、
Content-Length
ヘッダがない場合でも、Transfer-Encoding: chunked
を使用してボディを送信できます。
Content-Length
ヘッダ
Content-Length
ヘッダは、HTTPメッセージボディのオクテット(バイト)単位の長さを指定します。これは、受信側がメッセージボディの終わりを正確に判断するために重要です。
Content-Length: 0
: ボディが空であることを明示的に示します。Content-Length: -1
(Goの内部表現): Goのnet/http
パッケージでは、ContentLength
フィールドが-1
の場合、レスポンスボディの長さが不明であることを示します。この場合、HTTP/1.1ではTransfer-Encoding: chunked
を使用するか、Connection: close
ヘッダを付けて接続を閉じることでボディの終わりを示します。
Transfer-Encoding: chunked
ヘッダ
Transfer-Encoding: chunked
は、メッセージボディをチャンク(塊)に分割して送信するエンコーディング方式です。各チャンクは、そのチャンクのサイズを示す16進数と、それに続くデータで構成されます。ボディの終わりは、サイズが0のチャンクで示されます。この方式は、メッセージボディのサイズが事前に分からない場合や、動的に生成されるコンテンツをストリーミングする場合に特に有用です。Transfer-Encoding: chunked
が使用される場合、Content-Length
ヘッダは通常は送信されません。
Connection: close
ヘッダ
Connection
ヘッダは、現在のトランザクションが完了した後にネットワーク接続をどのように扱うかを制御します。
Connection: close
: 送信側(クライアントまたはサーバー)が、現在のトランザクションの完了後に接続を閉じたいことを示します。HTTP/1.0ではこれがデフォルトの動作でした。Connection: keep-alive
: HTTP/1.1のデフォルトであり、接続を維持して後続のリクエスト/レスポンスに再利用したいことを示します。
Content-Length
が不明なHTTP/1.1レスポンスで Transfer-Encoding: chunked
が使用されない場合、受信側は接続が閉じられることでボディの終わりを判断します。このため、Connection: close
ヘッダを明示的に設定する必要があります。
io.Reader
と io.Closer
(Go言語)
io.Reader
: データを読み出すためのインターフェースで、Read([]byte) (n int, err error)
メソッドを持ちます。io.Closer
: リソースを閉じるためのインターフェースで、Close() error
メソッドを持ちます。http.Response.Body
はio.ReadCloser
型であり、これはio.Reader
とio.Closer
の両方のインターフェースを満たします。レスポンスボディの読み出しと、そのリソースのクリーンアップ(接続の解放など)のために使用されます。
技術的詳細
このコミットの主要な技術的詳細は、http.Response.Write
メソッドが、HTTPレスポンスのボディの長さが不明な場合 (ContentLength: -1
) や、ボディが空である場合の挙動を正確に処理するように修正された点にあります。
修正前の Response.Write
は、これらのエッジケースにおいてHTTPプロトコルの仕様に準拠しないレスポンスを生成する可能性がありました。特に問題となっていたのは以下の点です。
ContentLength: 0
の扱い:Response.Body
がnil
または空のio.Reader
で、かつContentLength
が0
に設定されている場合、Content-Length: 0
ヘッダが適切に送信されない可能性がありました。ContentLength: -1
(不明な長さ) の扱い: HTTP/1.1において、ContentLength
が-1
で、かつTransfer-Encoding: chunked
が使用されていない場合、レスポンスボディの終わりを示すためにConnection: close
ヘッダを送信する必要があります。修正前は、このケースでConnection: close
が適切に設定されないことがありました。Body
の先読みとContentLength
の調整:Response.Body
が設定されているがContentLength
が0
または-1
の場合、Response.Write
はボディから少量のデータを先読みして、実際にボディが存在するかどうか、またはその長さが0
であるかどうかを判断しようとします。この先読みのロジックが不完全であったため、ボディが実際には空でないにもかかわらずContentLength: 0
と判断されたり、逆に空のボディに対して不適切な処理が行われたりする可能性がありました。
このコミットでは、これらの問題を解決するために以下の変更が加えられました。
Response
オブジェクトのクローン:Response.Write
メソッドの冒頭で、元のhttp.Response
オブジェクトのコピー (r1
) を作成し、このコピーに対して変更を加えるようにしました。これにより、元のResponse
オブジェクトが不意に変更されることを防ぎます。ContentLength: 0
の厳密な処理:r1.ContentLength == 0 && r1.Body != nil
のケースで、実際にボディが空であるかをr1.Body.Read(buf[:])
で1バイト読み出すことで確認します。- もし
Read
が0
バイトを返しio.EOF
であれば、ボディは本当に空であると判断し、r1.Body
をeofReader
(常にio.EOF
を返すリーダー) に設定し直します。これにより、後続の処理で空のボディが正しく扱われるようになります。 - もし
Read
が1
バイトを返した場合(つまりボディが空ではない場合)、r1.ContentLength
を-1
に設定し、r1.Body
をio.MultiReader
を使って先読みした1バイトと元のボディを結合したものに再構築します。これにより、ボディが空でないことが正しく伝わり、後続の処理でContentLength
が不明なボディとして扱われるようになります。
- もし
ContentLength: -1
とConnection: close
の自動設定:r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding)
の条件が満たされる場合(つまり、HTTP/1.1で長さ不明、かつConnection: close
が明示的に設定されておらず、チャンクエンコーディングも使用されていない場合)、r1.Close = true
を設定してConnection: close
ヘッダが送信されるようにします。これは、HTTP/1.1の仕様において、ボディの終わりを接続のクローズで示すための重要な修正です。Content-Length: 0
ヘッダの明示的な書き出し:r1.ContentLength == 0 && !chunked(r1.TransferEncoding)
の場合、Content-Length: 0
ヘッダを明示的に書き出すようにしました。これにより、ボディが空であることを受信側に確実に伝えます。- テストの拡充:
responsewrite_test.go
に多数の新しいテストケースが追加され、上記の様々なエッジケース(長さ不明のボディ、空のボディ、Connection: close
の挙動など)が網羅的に検証されるようになりました。これにより、将来的な回帰バグを防ぎ、Response.Write
の堅牢性を高めています。
これらの変更により、http.Response.Write
メソッドは、より多くのシナリオでHTTPプロトコルの仕様に準拠した正しいレスポンスを生成できるようになり、このメソッドを利用するアプリケーションの信頼性が向上しました。
コアとなるコードの変更箇所
src/pkg/net/http/response.go
の func (r *Response) Write(w io.Writer) error
メソッド内の変更がコアとなります。
--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -8,6 +8,7 @@ package http
import (
"bufio"
+ "bytes"
"crypto/tls"
"errors"
"io"
@@ -199,7 +200,6 @@ func (r *Response) ProtoAtLeast(major, minor int) bool {
//
// Body is closed after it is sent.
func (r *Response) Write(w io.Writer) error {
--
// Status line
text := r.Status
if text == "" {
@@ -212,10 +212,45 @@ func (r *Response) ProtoAtLeast(major, minor int) bool {
protoMajor, protoMinor := strconv.Itoa(r.ProtoMajor), strconv.Itoa(r.ProtoMinor)
statusCode := strconv.Itoa(r.StatusCode) + " "
text = strings.TrimPrefix(text, statusCode)
- io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n")
+ if _, err := io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n"); err != nil {
+ return err
+ }
+
+ // Clone it, so we can modify r1 as needed.
+ r1 := new(Response)
+ *r1 = *r
+ if r1.ContentLength == 0 && r1.Body != nil {
+ // Is it actually 0 length? Or just unknown?
+ var buf [1]byte
+ n, err := r1.Body.Read(buf[:])
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if n == 0 {
+ // Reset it to a known zero reader, in case underlying one
+ // is unhappy being read repeatedly.
+ r1.Body = eofReader
+ } else {
+ r1.ContentLength = -1
+ r1.Body = struct {
+ io.Reader
+ io.Closer
+ }{
+ io.MultiReader(bytes.NewReader(buf[:1]), r.Body),
+ r.Body,
+ }
+ }
+ }
+ // If we're sending a non-chunked HTTP/1.1 response without a
+ // content-length, the only way to do that is the old HTTP/1.0
+ // way, by noting the EOF with a connection close, so we need
+ // to set Close.
+ if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) {
+ r1.Close = true
+ }
// Process Body,ContentLength,Close,Trailer
- tw, err := newTransferWriter(r)
+ tw, err := newTransferWriter(r1)
if err != nil {
return err
}
@@ -230,8 +265,16 @@ func (r *Response) Write(w io.Writer) error {
return err
}
+ if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) {
+ if _, err := io.WriteString(w, "Content-Length: 0\\r\\n"); err != nil {
+ return err
+ }
+ }
+
// End-of-header
- io.WriteString(w, "\\r\\n")
+ if _, err := io.WriteString(w, "\\r\\n"); err != nil {
+ return err
+ }
// Write body and trailer
err = tw.WriteBody(w)
コアとなるコードの解説
src/pkg/net/http/response.go
の変更点
-
bytes
パッケージのインポート追加:+ "bytes"
bytes.NewReader
を使用するためにbytes
パッケージがインポートされました。これは、io.MultiReader
を使ってボディを再構築する際に必要となります。 -
ステータスライン書き込みのエラーハンドリング追加:
- io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n") + if _, err := io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n"); err != nil { + return err + }
ステータスラインの書き込み処理にエラーチェックが追加されました。これにより、書き込み中にエラーが発生した場合に早期に処理を終了し、エラーを返すようになります。
-
Response
オブジェクトのクローンとボディの長さの調整ロジック:// Clone it, so we can modify r1 as needed. r1 := new(Response) *r1 = *r if r1.ContentLength == 0 && r1.Body != nil { // Is it actually 0 length? Or just unknown? var buf [1]byte n, err := r1.Body.Read(buf[:]) if err != nil && err != io.EOF { return err } if n == 0 { // Reset it to a known zero reader, in case underlying one // is unhappy being read repeatedly. r1.Body = eofReader } else { r1.ContentLength = -1 r1.Body = struct { io.Reader io.Closer }{ io.MultiReader(bytes.NewReader(buf[:1]), r.Body), r.Body, } } }
r1 := new(Response); *r1 = *r
で、元のResponse
オブジェクトr
のシャローコピーr1
を作成します。これにより、Write
メソッド内でr1
を変更しても、呼び出し元のr
に影響を与えません。if r1.ContentLength == 0 && r1.Body != nil
の条件は、ContentLength
が0
と設定されているが、Body
がnil
ではない(つまり、ボディが存在する可能性がある)場合に実行されます。r1.Body.Read(buf[:])
でボディから1バイトを先読みします。n == 0
の場合(1バイトも読み込めず、io.EOF
が返された場合)、ボディは本当に空であると判断し、r1.Body
をeofReader
(常にio.EOF
を返す特別なリーダー) に設定し直します。これは、一部のio.Reader
実装が複数回読み込まれると問題を起こす可能性があるため、安全策として行われます。n > 0
の場合(1バイト読み込めた場合)、ボディは空ではないと判断し、r1.ContentLength
を-1
(長さ不明) に設定します。そして、io.MultiReader
を使用して、先読みした1バイトと元のボディを結合した新しいio.ReadCloser
をr1.Body
に設定します。これにより、先読みしたバイトが失われることなく、後続の処理でボディ全体が正しく読み込まれるようになります。
-
ContentLength: -1
の場合のConnection: close
自動設定ロジック:// If we're sending a non-chunked HTTP/1.1 response without a // content-length, the only way to do that is the old HTTP/1.0 // way, by noting the EOF with a connection close, so we need // to set Close. if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) { r1.Close = true }
この重要なブロックは、HTTP/1.1レスポンスにおいて、ボディの長さが不明 (
ContentLength == -1
) で、かつConnection: close
が明示的に設定されておらず (!r1.Close
)、チャンクエンコーディングも使用されていない (!chunked(r1.TransferEncoding)
) 場合に実行されます。この条件が満たされる場合、r1.Close = true
を設定し、レスポンスヘッダにConnection: close
が含まれるようにします。これにより、受信側は接続が閉じられることでボディの終わりを判断できるようになり、HTTP/1.1の仕様に準拠した正しい挙動となります。 -
newTransferWriter
の引数をr
からr1
へ変更:- tw, err := newTransferWriter(r) + tw, err := newTransferWriter(r1)
newTransferWriter
関数に渡すResponse
オブジェクトが、元のr
から、修正が加えられたクローンr1
に変更されました。これにより、上記のロジックで調整されたr1
のContentLength
やClose
の値が、転送の書き出し処理に正しく反映されるようになります。 -
Content-Length: 0
ヘッダの明示的な書き出し:if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) { if _, err := io.WriteString(w, "Content-Length: 0\\r\\n"); err != nil { return err } }
r1.ContentLength
が0
で、かつチャンクエンコーディングが使用されていない場合、Content-Length: 0
ヘッダを明示的に書き出すようにしました。これにより、ボディが空であることを受信側に確実に伝えます。 -
ヘッダ終了を示す空行書き込みのエラーハンドリング追加:
- io.WriteString(w, "\\r\\n") + if _, err := io.WriteString(w, "\\r\\n"); err != nil { + return err + }
ヘッダの終わりを示す空行 (
\r\n
) の書き込み処理にもエラーチェックが追加されました。
src/pkg/net/http/response_test.go
の変更点
dummyReq11
関数の追加:
HTTP/1.1プロトコルを使用するダミーのfunc dummyReq11(method string) *Request { return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1} }
http.Request
オブジェクトを生成するためのヘルパー関数が追加されました。これにより、HTTP/1.1に特化したテストケースを簡潔に記述できるようになります。
src/pkg/net/http/responsewrite_test.go
の変更点
-
ioutil.NopCloser(bytes.NewBufferString("abcdef"))
からioutil.NopCloser(strings.NewReader("abcdef"))
への変更:- Body: ioutil.NopCloser(bytes.NewBufferString("abcdef")), + Body: ioutil.NopCloser(strings.NewReader("abcdef")),
テストケースで使用されるボディの生成方法が
bytes.NewBufferString
からstrings.NewReader
に変更されました。機能的には大きな違いはありませんが、strings.NewReader
の方が文字列からio.Reader
を生成する意図が明確です。 -
多数の新しいテストケースの追加: このファイルには、
Response.Write
の様々なエッジケースを検証するための、非常に多くの新しいテストケースが追加されています。これには、以下のようなシナリオが含まれます。ContentLength
が不明 (-1
) で、Connection: close
が明示的に設定されている場合とそうでない場合のHTTP/1.1レスポンス。ContentLength
が不明 (-1
) で、Transfer-Encoding: chunked
が使用されているHTTP/1.1レスポンス。ContentLength: 0
で、ボディがnil
の場合、空のio.Reader
の場合、非空のio.Reader
の場合のHTTP/1.1レスポンス。 これらのテストケースは、Response.Write
メソッドがHTTPプロトコルの複雑なルールに正確に準拠していることを保証するために不可欠です。
関連リンク
参考にした情報源リンク
- HTTP Content-Length Header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
- HTTP Transfer-Encoding Header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding
- HTTP Connection Header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
- Go
net/http
Response.Write
purpose: https://pkg.go.dev/net/http#Response.Write (これは一般的な情報源として使用しましたが、特定のバグに関する直接的な情報ではありません) - Go
io
package: https://pkg.go.dev/io