[インデックス 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/httpResponse.Writepurpose: https://pkg.go.dev/net/http#Response.Write (これは一般的な情報源として使用しましたが、特定のバグに関する直接的な情報ではありません) - Go
iopackage: https://pkg.go.dev/io