[インデックス 10419] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおけるHTTPレスポンスボディのEOF (End-Of-File) 処理に関するバグ修正です。具体的には、http.Response
のボディがEOFに達した際に (0, nil)
を返すという io.Reader
インターフェースの契約違反を修正し、テストケースを追加することでこの問題が再発しないようにしています。
コミット
net/http: fix EOF handling on response body
http.Response is currently returning 0, nil on EOF.
R=golang-dev, bradfitz, bradfitz
CC=golang-dev
https://golang.org/cl/5394047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c9355596cde33fe025e801066ee718a9941124c9
元コミット内容
net/http
パッケージにおいて、HTTPレスポンスのボディを読み込む際に、EOF (End-Of-File) に達した場合の挙動が io.Reader
インターフェースの期待する契約に違反していました。具体的には、io.Reader
の Read
メソッドは、EOFに達した際には (n, io.EOF)
の形式で値を返すことが期待されます(n
は読み込んだバイト数で、0またはそれ以上)。しかし、このバグでは (0, nil)
を返していました。これは、読み込み側がEOFを正しく検出できず、無限ループに陥ったり、データがまだあると誤解したりする原因となります。このコミットは、この誤ったEOF処理を修正することを目的としています。
変更の背景
Go言語の io.Reader
インターフェースは、データのストリームを読み込むための基本的な抽象化を提供します。このインターフェースの Read
メソッドには厳密な契約があり、特にEOFに達した場合の戻り値に関する規定があります。
Read(p []byte) (n int, err error)
n
はp
に読み込まれたバイト数です。err
は読み込み中に発生したエラーです。- EOFの場合: ストリームの終端に達した場合、
Read
は(0, io.EOF)
を返すか、または最後に読み込んだデータと共に(n > 0, io.EOF)
を返すことができます。重要なのは、EOFに達した際にはio.EOF
エラーを返すという点です。n=0
かつerr=nil
は、データがまだ利用可能であるにもかかわらず、一時的に読み込むデータがない場合にのみ発生すべきであり、EOFを示すものではありません。
当時の net/http
パッケージの Response.Body
は、EOFに達した際に (0, nil)
を返していました。これは io.Reader
の契約に違反しており、ioutil.ReadAll
のような io.Reader
の契約に依存する関数が、EOFを正しく検出できずに無限ループに陥る可能性がありました。このコミットは、この契約違反を修正し、net/http
が io.Reader
の期待する挙動に準拠するようにするためのものです。
前提知識の解説
io.Reader
インターフェース
Go言語における io.Reader
インターフェースは、バイトのストリームを読み込むための最も基本的なインターフェースです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドは、最大len(p)
バイトをp
に読み込み、読み込んだバイト数n
とエラーerr
を返します。- 重要な契約:
n > 0
の場合、Read
はnil
以外のエラーを返すこともできますが、通常はnil
を返します。n == 0
の場合、Read
はエラーを返す必要があります。- ストリームの終端に達した場合は
io.EOF
を返します。 - 一時的な条件(例: 非ブロッキングI/Oでデータがまだ利用できない場合)の場合は、
io.EOF
以外のエラー(例:io.ErrNoProgress
やsyscall.EAGAIN
)を返します。
- ストリームの終端に達した場合は
n == 0
かつerr == nil
は、Read
が何も読み込まず、かつエラーも発生しなかったことを意味します。これは、データがまだ利用可能であるにもかかわらず、一時的に読み込むデータがない場合にのみ発生すべきです。EOFを示すものではありません。
HTTPレスポンスボディの読み込み
HTTPクライアントがサーバーからレスポンスを受け取ると、そのボディは通常 io.Reader
として提供されます(例: http.Response.Body
)。アプリケーションは、この io.Reader
を使ってレスポンスボディのデータを読み込みます。ioutil.ReadAll
のようなヘルパー関数は、この io.Reader
の契約に厳密に依存して動作します。
Goにおけるエラーハンドリング
Goでは、エラーは関数の最後の戻り値として error
型で返されます。nil
はエラーがないことを意味します。io.EOF
は、io
パッケージで定義されている特別なエラー値で、ストリームの終端を示すために使用されます。
技術的詳細
このコミットは、主に2つのファイルにわたる変更を含んでいます。
src/pkg/net/http/client_test.go
:io.Reader
の契約を厳密にテストするための新しいヘルパー関数pedanticReadAll
が追加されました。src/pkg/net/http/transfer.go
:http.Response
のボディを実装するbody
型のRead
メソッドが修正され、EOF時のio.Reader
契約違反が解消されました。
pedanticReadAll
の導入
client_test.go
に追加された pedanticReadAll
関数は、ioutil.ReadAll
と同様に io.Reader
からすべてのバイトを読み込みますが、io.Reader
の契約、特にEOF時の挙動を厳密に検証します。
// pedanticReadAll works like ioutil.ReadAll but additionally
// verifies that r obeys the documented io.Reader contract.
func pedanticReadAll(r io.Reader) (b []byte, err error) {
var bufa [64]byte
buf := bufa[:]
for {
n, err := r.Read(buf)
if n == 0 && err == nil {
return nil, fmt.Errorf("Read: n=0 with err=nil") // ここで契約違反を検出
}
b = append(b, buf[:n]...)
if err == io.EOF {
// EOF後に再度Readを呼び出した場合の挙動も検証
n, err := r.Read(buf)
if n != 0 || err != io.EOF {
return nil, fmt.Errorf("Read: n=%d err=%#v after EOF", n, err)
}
return b, nil
}
if err != nil {
return b, err
}
}
panic("unreachable")
}
この関数は、以下の2つの重要なチェックを行います。
if n == 0 && err == nil
: これがこのコミットの核心です。io.Reader
の契約では、n=0
かつerr=nil
はEOFを示すものではありません。この条件が満たされた場合、pedanticReadAll
はエラーを返し、テストを失敗させます。これにより、http.Response.Body
が以前行っていた誤ったEOF処理を検出できるようになります。- EOF後の
Read
呼び出し:io.EOF
が返された後、Read
を再度呼び出した場合、常に(0, io.EOF)
を返す必要があります。この関数は、この挙動も検証し、契約違反がないことを確認します。
既存の TestClient
関数では、ioutil.ReadAll(r.Body)
の代わりに pedanticReadAll(r.Body)
が使用されるようになり、http.Response.Body
のEOF処理が正しく行われているかを厳密にテストできるようになりました。
transfer.go
における body.Read
の修正
transfer.go
ファイルには、HTTPレスポンスボディの読み込みロジックを実装する body
型の Read
メソッドが含まれています。このメソッドの修正は、EOFに達した際にトレーラー(HTTP/1.1のチャンク転送エンコーディングで、ボディの終わりに続く追加のヘッダー)を読み込む部分にあります。
修正前は、b.readTrailer()
がエラーを返しても、そのエラーが Read
メソッドの戻り値 err
に適切に伝播されない可能性がありました。
// 修正前
if err == io.EOF && b.hdr != nil {
err = b.readTrailer() // ここで返されたエラーが、外側のerrに代入されるが、
// その後errがnilに上書きされる可能性があったり、
// そもそもreadTrailerがnilを返した場合に問題が残る
}
return n, err // ここでerrがnilのまま返される可能性
修正後は、b.readTrailer()
がエラーを返した場合に、そのエラーを明示的に err
変数に代入し、io.EOF
を上書きするように変更されています。
// 修正後
if err == io.EOF && b.hdr != nil {
if e := b.readTrailer(); e != nil { // readTrailerのエラーをeに格納
err = e // eがnilでなければ、errをeで上書き
}
b.hdr = nil
}
return n, err
この変更により、readTrailer
がエラーを返した場合(例えば、不正なトレーラー形式など)、そのエラーが body.Read
の呼び出し元に正しく伝播されるようになります。これにより、io.Reader
の契約である「n=0
かつ err=nil
はEOFではない」という原則が守られ、io.EOF
以外のエラーが発生した場合にはそれが正しく報告されるようになります。
コアとなるコードの変更箇所
src/pkg/net/http/client_test.go
--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -26,6 +26,31 @@ var robotsTxtHandler = HandlerFunc(func(w ResponseWriter, r *Request) {
fmt.Fprintf(w, "User-agent: go\nDisallow: /something/")
})
+// pedanticReadAll works like ioutil.ReadAll but additionally
+// verifies that r obeys the documented io.Reader contract.
+func pedanticReadAll(r io.Reader) (b []byte, err error) {
+ var bufa [64]byte
+ buf := bufa[:]
+ for {
+ n, err := r.Read(buf)
+ if n == 0 && err == nil {
+ return nil, fmt.Errorf("Read: n=0 with err=nil")
+ }
+ b = append(b, buf[:n]...)
+ if err == io.EOF {
+ n, err := r.Read(buf)
+ if n != 0 || err != io.EOF {
+ return nil, fmt.Errorf("Read: n=%d err=%#v after EOF", n, err)
+ }
+ return b, nil
+ }
+ if err != nil {
+ return b, err
+ }
+ }
+ panic("unreachable")
+}
+
func TestClient(t *testing.T) {
ts := httptest.NewServer(robotsTxtHandler)
defer ts.Close()
@@ -33,7 +58,7 @@ func TestClient(t *t.T) {
r, err := Get(ts.URL)
var b []byte
if err == nil {
- b, err = ioutil.ReadAll(r.Body)
+ b, err = pedanticReadAll(r.Body)
r.Body.Close()
}
if err != nil {
src/pkg/net/http/transfer.go
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -537,7 +537,9 @@ func (b *body) Read(p []byte) (n int, err error) {
// Read the final trailer once we hit EOF.
if err == io.EOF && b.hdr != nil {
- err = b.readTrailer()
+ if e := b.readTrailer(); e != nil {
+ err = e
+ }
b.hdr = nil
}
return n, err
コアとなるコードの解説
client_test.go
の変更
pedanticReadAll
関数の追加: この関数は、io.Reader
のRead
メソッドがn=0
かつerr=nil
を返した場合にエラーを発生させることで、io.Reader
の契約違反を厳密にチェックします。また、EOFが返された後に再度Read
を呼び出した場合の挙動(常に(0, io.EOF)
を返すこと)も検証します。TestClient
の修正: 既存のテスト関数TestClient
内で、ioutil.ReadAll(r.Body)
の代わりに新しく追加されたpedanticReadAll(r.Body)
を使用するように変更されています。これにより、HTTPレスポンスボディのEOF処理がio.Reader
の契約に準拠しているかどうかが、より厳密にテストされるようになりました。
transfer.go
の変更
body.Read
メソッドの修正: この修正は、HTTPレスポンスボディの読み込みがEOFに達し、かつトレーラーヘッダーが存在する場合の処理に関するものです。- 修正前は、
b.readTrailer()
がエラーを返しても、そのエラーがRead
メソッドの最終的な戻り値err
に適切に伝播されない可能性がありました。 - 修正後は、
if e := b.readTrailer(); e != nil { err = e }
という形で、readTrailer
が返したエラーe
がnil
でない場合に、そのエラーをRead
メソッドの戻り値err
に明示的に代入するように変更されています。 - これにより、トレーラーの読み込み中に発生したエラー(例えば、不正なトレーラー形式など)が、
io.EOF
ではなく、その具体的なエラーとして呼び出し元に正しく報告されるようになります。これは、io.Reader
の契約である「n=0
かつerr=nil
はEOFではない」という原則を維持し、io.EOF
以外のエラーが発生した場合にはそれが正しく報告されるようにするために重要です。
- 修正前は、
これらの変更により、net/http
パッケージは io.Reader
インターフェースの契約に完全に準拠し、HTTPレスポンスボディのEOF処理がより堅牢になりました。
関連リンク
- Go CL 5394047: https://golang.org/cl/5394047
参考にした情報源リンク
- Go Documentation:
io.Reader
Interface: https://pkg.go.dev/io#Reader - Go Documentation:
io.EOF
Variable: https://pkg.go.dev/io#pkg-variables - Go Blog: Errors are values: https://go.dev/blog/errors-are-values
- Go Blog: The Go net/http package: https://go.dev/blog/go-and-the-web
- Stack Overflow: Why does io.Reader.Read return (0, nil) sometimes?: https://stackoverflow.com/questions/23077108/why-does-io-reader-read-return-0-nil-sometimes
- Go issue: net/http: Response.Body returns (0, nil) on EOF: https://github.com/golang/go/issues/2439 (このコミットが修正した問題のIssue)