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

[インデックス 18258] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける、HTTPレスポンスの読み込み時のエラーハンドリングに関する修正です。具体的には、レスポンスが途中で切断された(truncated)場合に、io.EOF ではなく io.ErrUnexpectedEOF を返すように変更されています。これにより、クライアント側で予期せぬ接続終了と正常なストリーム終了を区別できるようになり、より堅牢なエラーハンドリングが可能になります。

コミット

commit 89c9d6b7f858cea20a4f564d88ff7831c4375403
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Jan 14 19:08:40 2014 -0800

    net/http: return UnexpectedEOF instead of EOF on truncated resposne
    
    Fixes #6564
    
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/52420043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/89c9d6b7f858cea20a4f564d88ff7831c4375403

元コミット内容

このコミットの元の内容は、net/http パッケージの ReadResponse 関数が、HTTPレスポンスのヘッダーを読み込む際に、予期せぬEOF(End Of File)に遭遇した場合に io.EOF を返していた問題を修正することです。本来、データが途中で途切れた場合は io.ErrUnexpectedEOF を返すのが適切であり、このコミットはその挙動を修正しています。

変更の背景

この変更の背景には、Go言語の io パッケージにおけるエラーのセマンティクスと、HTTPプロトコルにおけるレスポンスの完全性の問題があります。

Go言語の io パッケージでは、io.EOF は「ストリームの終端に到達し、それ以上読み取るデータがない」という正常な状態を示すために使用されます。例えば、ファイルの内容を最後まで読み込んだ場合や、HTTPレスポンスボディの最後まで読み込んだ場合などがこれに該当します。

一方、io.ErrUnexpectedEOF は、「予期せぬストリームの終端に到達した」というエラー状態を示すために使用されます。これは、期待されるデータ量に満たない状態でストリームが終了した場合や、プロトコルが期待するデータの途中で接続が切断された場合などに発生します。

net/http パッケージの ReadResponse 関数は、HTTPレスポンスのヘッダーを読み込む際に、ネットワーク接続が途中で切断された場合、つまりレスポンスが不完全に送信された場合に io.EOF を返していました。しかし、これはHTTPプロトコルの観点から見ると「予期せぬ終端」であり、io.ErrUnexpectedEOF を返すのがより適切でした。

この問題は、GoのIssue #6564で報告されており、クライアント側がレスポンスの不完全性を正確に判断できないという問題を引き起こしていました。例えば、プロキシサーバーが途中で接続を切断した場合や、サーバーが不完全なレスポンスを送信した場合に、クライアントはそれを正常なレスポンスの終端と誤解してしまう可能性がありました。この修正により、クライアントは io.ErrUnexpectedEOF を受け取ることで、レスポンスが不完全であることを明確に認識し、適切なエラーハンドリングを行うことができるようになります。

前提知識の解説

Go言語の io パッケージとエラー

Go言語の io パッケージは、I/O操作のための基本的なインターフェースとエラーを定義しています。

  • io.Reader インターフェース: データを読み取るための基本的なインターフェースです。Read(p []byte) (n int, err error) メソッドを持ち、n は読み取ったバイト数、err はエラーを示します。
  • io.EOF: var EOF = errors.New("EOF") として定義されているエラー変数です。これは、Reader がストリームの終端に到達し、それ以上データがないことを示すために返されます。これはエラーというよりは、正常な終了条件を示すシグナルとして扱われます。
  • io.ErrUnexpectedEOF: var ErrUnexpectedEOF = errors.New("unexpected EOF") として定義されているエラー変数です。これは、Reader が予期せぬストリームの終端に到達したことを示すために返されます。例えば、期待されるバイト数を読み取る前にストリームが終了した場合などに使用されます。

これらのエラーを適切に区別することは、I/O操作の正確な状態を把握し、適切なエラーリカバリやロギングを行う上で非常に重要です。

HTTPプロトコルとレスポンスの構造

HTTP(Hypertext Transfer Protocol)は、クライアントとサーバー間でデータを交換するためのプロトコルです。HTTPレスポンスは、通常、以下の要素で構成されます。

  1. ステータスライン: HTTPバージョン、ステータスコード、ステータスメッセージ(例: HTTP/1.1 200 OK)。
  2. ヘッダー: キーと値のペアのリストで、レスポンスに関するメタデータを提供します(例: Content-Type: text/html, Content-Length: 1234)。ヘッダーの終わりは空行で示されます。
  3. ボディ: 実際のデータ(HTML、JSON、画像など)。

net/http パッケージの ReadResponse 関数は、これらの要素を順に読み取っていきます。特にヘッダーの読み取りは、空行が現れるまで続くため、その途中で接続が切断された場合、それはプロトコル違反であり、「予期せぬEOF」と見なされるべきです。

技術的詳細

このコミットの技術的な詳細は、net/http パッケージの ReadResponse 関数におけるエラーハンドリングの改善にあります。

ReadResponse 関数は、bufio.Reader を介してネットワークからデータを読み取ります。この関数は、まずステータスラインを解析し、次にHTTPヘッダーを解析します。ヘッダーの解析には、内部的に textproto.ReaderReadMIMEHeader メソッドが使用されます。

修正前のコードでは、ReadMIMEHeaderio.EOF を返した場合、ReadResponse もそのまま io.EOF を返していました。しかし、HTTPヘッダーの読み取り中に io.EOF が発生するということは、ヘッダーが完全に送信される前に接続が切断されたことを意味します。これは、HTTPプロトコルに違反する不完全なレスポンスであり、正常なストリームの終端を示す io.EOF ではなく、予期せぬエラーを示す io.ErrUnexpectedEOF を返すのが適切です。

このコミットでは、この挙動を修正するために、ReadMIMEHeader から返されたエラーが io.EOF であった場合に、それを io.ErrUnexpectedEOF にラップして返すように変更されています。

// src/pkg/net/http/response.go
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {
    // ...
    // Parse the response headers.
    mimeHeader, err := tp.ReadMIMEHeader()
    if err != nil {
        if err == io.EOF { // ここでio.EOFをチェック
            err = io.ErrUnexpectedEOF // io.ErrUnexpectedEOFに変換
        }
        return nil, err
    }
    // ...
}

この変更により、ReadResponse の呼び出し元は、HTTPレスポンスのヘッダーが不完全であった場合に、明確に io.ErrUnexpectedEOF を受け取ることができます。これにより、クライアントアプリケーションは、ネットワークの問題やサーバーの不適切な挙動をより正確に検出し、適切なエラー処理(例えば、再試行、エラーログの記録、ユーザーへの通知など)を行うことが可能になります。

また、この変更は response_test.go に新しいテストケース TestReadResponseUnexpectedEOF を追加することで検証されています。このテストは、不完全なHTTPレスポンスヘッダー(Locationヘッダーが途中で切れている)を ReadResponse に与え、期待通り io.ErrUnexpectedEOF が返されることを確認しています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、src/pkg/net/http/response.go ファイル内の ReadResponse 関数と、それに対応するテストケースが追加された src/pkg/net/http/response_test.go ファイルです。

src/pkg/net/http/response.go

--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -141,6 +141,9 @@ func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {
 	// Parse the response headers.
 	mimeHeader, err := tp.ReadMIMEHeader()
 	if err != nil {
+		if err == io.EOF {
+			err = io.ErrUnexpectedEOF
+		}
 		return nil, err
 	}
 	resp.Header = Header(mimeHeader)

src/pkg/net/http/response_test.go

--- a/src/pkg/net/http/response_test.go
+++ b/src/pkg/net/http/response_test.go
@@ -618,6 +618,15 @@ func TestResponseContentLengthShortBody(t *testing.T) {
 	}\n}\n\n+func TestReadResponseUnexpectedEOF(t *testing.T) {\n+\tbr := bufio.NewReader(strings.NewReader(\"HTTP/1.1 301 Moved Permanently\\r\\n\" +\n+\t\t\"Location: http://example.com\"))\n+\t_, err := ReadResponse(br, nil)\n+\tif err != io.ErrUnexpectedEOF {\n+\t\tt.Errorf(\"ReadResponse = %v; want io.ErrUnexpectedEOF\", err)\n+\t}\n+}\n+\n func TestNeedsSniff(t *testing.T) {
 	// needsSniff returns true with an empty response.
 	r := &response{}

コアとなるコードの解説

src/pkg/net/http/response.go の変更

ReadResponse 関数は、HTTPレスポンスを読み取り、*Response オブジェクトを構築する役割を担っています。この関数内で、tp.ReadMIMEHeader() が呼び出され、HTTPヘッダーが解析されます。

変更前のコードでは、tp.ReadMIMEHeader() がエラーを返した場合、そのエラーがそのまま ReadResponse の呼び出し元に返されていました。もしこのエラーが io.EOF であった場合、それは「ストリームの正常な終端」として扱われてしまう可能性がありました。

今回の修正では、if err != nil のブロック内に以下の3行が追加されました。

		if err == io.EOF {
			err = io.ErrUnexpectedEOF
		}

このコードは、tp.ReadMIMEHeader() から返されたエラーが io.EOF と等しいかどうかをチェックしています。もし等しければ、そのエラー変数を io.ErrUnexpectedEOF に上書きしています。これにより、HTTPヘッダーの読み取り中に予期せぬストリームの終端が発生した場合、呼び出し元には io.ErrUnexpectedEOF が返されるようになります。これは、HTTPプロトコルが期待するヘッダーの完全性が満たされなかったことを明確に示します。

src/pkg/net/http/response_test.go の追加テスト

TestReadResponseUnexpectedEOF という新しいテスト関数が追加されました。このテストの目的は、ReadResponse 関数が不完全なHTTPレスポンスヘッダーを受け取った際に、正しく io.ErrUnexpectedEOF を返すことを検証することです。

テストケースでは、strings.NewReader を使用して、意図的に不完全なHTTPレスポンス文字列を作成しています。

	br := bufio.NewReader(strings.NewReader("HTTP/1.1 301 Moved Permanently\r\n" +
		"Location: http://example.com"))

この文字列は、Location ヘッダーの値が途中で切れています。ReadResponse はこの不完全なヘッダーを読み取ろうとしますが、Location ヘッダーの終端を示す \r\n がないため、io.EOF に遭遇します。

テストでは、ReadResponse(br, nil) を呼び出し、返されたエラーが io.ErrUnexpectedEOF であることをアサートしています。

	_, err := ReadResponse(br, nil)
	if err != io.ErrUnexpectedEOF {
		t.Errorf("ReadResponse = %v; want io.ErrUnexpectedEOF", err)
	}

このテストの追加により、修正が正しく機能していること、そして将来のリグレッションを防ぐための安全網が提供されます。

関連リンク

参考にした情報源リンク