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

[インデックス 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.ReaderRead メソッドは、EOFに達した際には (n, io.EOF) の形式で値を返すことが期待されます(n は読み込んだバイト数で、0またはそれ以上)。しかし、このバグでは (0, nil) を返していました。これは、読み込み側がEOFを正しく検出できず、無限ループに陥ったり、データがまだあると誤解したりする原因となります。このコミットは、この誤ったEOF処理を修正することを目的としています。

変更の背景

Go言語の io.Reader インターフェースは、データのストリームを読み込むための基本的な抽象化を提供します。このインターフェースの Read メソッドには厳密な契約があり、特にEOFに達した場合の戻り値に関する規定があります。

  • Read(p []byte) (n int, err error)
    • np に読み込まれたバイト数です。
    • 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/httpio.Reader の期待する挙動に準拠するようにするためのものです。

前提知識の解説

io.Reader インターフェース

Go言語における io.Reader インターフェースは、バイトのストリームを読み込むための最も基本的なインターフェースです。

type Reader interface {
    Read(p []byte) (n int, err error)
}
  • Read メソッドは、最大 len(p) バイトを p に読み込み、読み込んだバイト数 n とエラー err を返します。
  • 重要な契約:
    • n > 0 の場合、Readnil 以外のエラーを返すこともできますが、通常は nil を返します。
    • n == 0 の場合、Read はエラーを返す必要があります。
      • ストリームの終端に達した場合は io.EOF を返します。
      • 一時的な条件(例: 非ブロッキングI/Oでデータがまだ利用できない場合)の場合は、io.EOF 以外のエラー(例: io.ErrNoProgresssyscall.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つのファイルにわたる変更を含んでいます。

  1. src/pkg/net/http/client_test.go: io.Reader の契約を厳密にテストするための新しいヘルパー関数 pedanticReadAll が追加されました。
  2. 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.ReaderRead メソッドが 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 が返したエラー enil でない場合に、そのエラーを Read メソッドの戻り値 err に明示的に代入するように変更されています。
    • これにより、トレーラーの読み込み中に発生したエラー(例えば、不正なトレーラー形式など)が、io.EOF ではなく、その具体的なエラーとして呼び出し元に正しく報告されるようになります。これは、io.Reader の契約である「n=0 かつ err=nil はEOFではない」という原則を維持し、io.EOF 以外のエラーが発生した場合にはそれが正しく報告されるようにするために重要です。

これらの変更により、net/http パッケージは io.Reader インターフェースの契約に完全に準拠し、HTTPレスポンスボディのEOF処理がより堅牢になりました。

関連リンク

参考にした情報源リンク