[インデックス 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レスポンスは、通常、以下の要素で構成されます。
- ステータスライン: HTTPバージョン、ステータスコード、ステータスメッセージ(例:
HTTP/1.1 200 OK
)。 - ヘッダー: キーと値のペアのリストで、レスポンスに関するメタデータを提供します(例:
Content-Type: text/html
,Content-Length: 1234
)。ヘッダーの終わりは空行で示されます。 - ボディ: 実際のデータ(HTML、JSON、画像など)。
net/http
パッケージの ReadResponse
関数は、これらの要素を順に読み取っていきます。特にヘッダーの読み取りは、空行が現れるまで続くため、その途中で接続が切断された場合、それはプロトコル違反であり、「予期せぬEOF」と見なされるべきです。
技術的詳細
このコミットの技術的な詳細は、net/http
パッケージの ReadResponse
関数におけるエラーハンドリングの改善にあります。
ReadResponse
関数は、bufio.Reader
を介してネットワークからデータを読み取ります。この関数は、まずステータスラインを解析し、次にHTTPヘッダーを解析します。ヘッダーの解析には、内部的に textproto.Reader
の ReadMIMEHeader
メソッドが使用されます。
修正前のコードでは、ReadMIMEHeader
が io.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)
}
このテストの追加により、修正が正しく機能していること、そして将来のリグレッションを防ぐための安全網が提供されます。
関連リンク
- Go Issue #6564: net/http: ReadResponse returns EOF instead of UnexpectedEOF on truncated response
- Go Code Review: https://golang.org/cl/52420043
参考にした情報源リンク
- Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - HTTP/1.1 RFC 2616 (Hypertext Transfer Protocol -- HTTP/1.1): https://datatracker.ietf.org/doc/html/rfc2616
- Go言語のエラーハンドリングに関する一般的な情報源 (例: Go by Example - Errors): https://gobyexample.com/errors
- Go言語のテストに関する一般的な情報源 (例: Go by Example - Testing): https://gobyexample.com/testing