[インデックス 12567] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける ReadRequest
関数が、HTTPリクエストの読み込み中に適切なエラーを返さない問題を修正するものです。具体的には、io.EOF
と io.ErrUnexpectedEOF
の扱いを改善し、より正確なエラー報告を行うように変更されています。これにより、HTTPサーバーがクライアントからの不正なリクエストや接続切断に対して、より適切に対応できるようになります。
コミット
commit e8deb3f828886afe3dc7403f128cbafebe9fb1a1
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 12 10:42:25 2012 -0700
net/http: return appropriate errors from ReadRequest
Fixes #3298
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5783080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e8deb3f828886afe3dc7403f128cbafebe9fb1a1
元コミット内容
net/http
パッケージの ReadRequest
関数が、適切なエラーを返すように修正されました。
この変更は、Issue #3298 を修正します。
変更の背景
このコミットは、Go言語のIssue #3298「net/http
: wrong errors from ReadRequest」を解決するために行われました。
元の ReadRequest
関数では、HTTPリクエストの最初の行(例: GET /index.html HTTP/1.0
)を読み込む際に、bufio.Reader
から io.EOF
エラーが返された場合、それを無条件に io.ErrUnexpectedEOF
に変換していました。
しかし、HTTP/1.1の仕様では、クライアントがリクエストボディを送信する前に接続を閉じる(つまり、リクエストの最初の行を読み込んだ直後にEOFを受け取る)ことは、必ずしも「予期せぬEOF」ではありません。例えば、クライアントがリクエストヘッダのみを送信し、その後すぐに接続を閉じるようなケースでは、io.EOF
が返されるのが自然です。このような場合に io.ErrUnexpectedEOF
を返してしまうと、サーバー側でエラーハンドリングが不正確になり、デバッグが困難になる可能性がありました。
また、net/http/server.go
の conn.serve()
メソッドでは、ReadRequest
から返されるエラーに基づいてクライアントへの応答を決定していましたが、ここでも io.ErrUnexpectedEOF
と io.EOF
の区別が適切に行われていませんでした。特に、io.ErrUnexpectedEOF
の場合に「413 Request Entity Too Large」というメッセージを返す可能性がありましたが、これは本来 io.EOF
の場合に「Don't reply」(応答しない)とすべき状況と混同される可能性がありました。
このコミットは、これらの問題を修正し、ReadRequest
がより正確なエラーを返し、それに基づいてサーバーが適切に動作するようにすることを目的としています。
前提知識の解説
- Go言語の
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーを実装するための機能を提供します。Webアプリケーション開発において中心的な役割を果たします。 - HTTPリクエストの構造: HTTPリクエストは、通常、リクエストライン、ヘッダ、空行、そしてオプションのリクエストボディで構成されます。
- リクエストライン:
メソッド URI HTTPバージョン
の形式(例:GET /index.html HTTP/1.0
)。 - ヘッダ:
キー: 値
の形式で、リクエストに関する追加情報を提供します。 - 空行: ヘッダの終わりを示します。
- リクエストボディ: POSTリクエストなどでデータを送信する場合に使用されます。
- リクエストライン:
bufio.Reader
:io.Reader
インターフェースをラップし、バッファリングされたI/Oを提供します。これにより、効率的な読み込みが可能になります。ReadLine()
メソッドは、改行文字までを読み込みます。io.EOF
:io
パッケージで定義されているエラーで、入力の終わりに達したことを示します。通常、ファイルやストリームの最後まで読み込んだ場合に返されます。io.ErrUnexpectedEOF
:io
パッケージで定義されているエラーで、予期せぬ入力の終わりに達したことを示します。通常、完全なデータブロックが期待されるにもかかわらず、途中で入力が終了した場合に返されます。例えば、固定長のデータを読み込んでいる途中でEOFに達した場合などです。defer
ステートメント: Go言語のキーワードで、defer
に続く関数呼び出しを、その関数がリターンする直前に実行するようにスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除など)やエラーハンドリングによく使用されます。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/http/request.go
内の ReadRequest
関数におけるエラーハンドリングの改善です。
変更前は、tp.ReadLine()
(HTTPリクエストの最初の行を読み込む)がエラーを返した場合、そのエラーが io.EOF
であれば io.ErrUnexpectedEOF
に変換し、それ以外の場合はそのままエラーを返していました。このロジックは、HTTPリクエストの最初の行が途中で切れることは常に「予期せぬ」状況であるという前提に立っていました。
しかし、前述の通り、クライアントがリクエストヘッダのみを送信して接続を閉じるようなケースでは、io.EOF
が返されるのは自然な動作であり、これを io.ErrUnexpectedEOF
に変換するのは不適切でした。
このコミットでは、以下の修正が行われました。
-
defer
ステートメントの導入:ReadRequest
関数の冒頭でtp.ReadLine()
がエラーを返した場合、すぐにreturn nil, err
するように変更されました。 その直後にdefer
関数が導入され、このdefer
関数内でerr == io.EOF
の場合にのみerr = io.ErrUnexpectedEOF
と再割り当てするロジックが移動されました。 これにより、tp.ReadLine()
がio.EOF
を返した場合でも、即座にio.EOF
が返されるようになり、その後の処理でio.ErrUnexpectedEOF
に変換されるのは、リクエストの他の部分(ヘッダやボディ)を読み込んでいる途中でio.EOF
が発生した場合に限定されるようになりました。 -
net/http/server.go
のエラーハンドリング修正:conn.serve()
メソッド内で、ReadRequest
から返されたエラーがio.ErrUnexpectedEOF
の場合にmsg = "413 Request Entity Too Large"
としていた部分が、io.EOF
の場合にbreak
(応答しない)とするように変更されました。 これは、クライアントがリクエストの途中で接続を閉じた(io.EOF
)場合、サーバーは応答すべきではないというHTTPのセマンティクスに合致させるための修正です。413 Request Entity Too Large
は、リクエストボディが大きすぎる場合に返すステータスコードであり、EOFとは直接関係ありません。
これらの変更により、net/http
パッケージはHTTPプロトコルの仕様により厳密に準拠し、より堅牢なエラーハンドリングを提供するようになりました。
コアとなるコードの変更箇所
src/pkg/net/http/request.go
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -455,11 +455,13 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
// First line: GET /index.html HTTP/1.0
var s string
if s, err = tp.ReadLine(); err != nil {
+ return nil, err
+ }
+ defer func() {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
- return nil, err
- }
+ }()
var f []string
if f = strings.SplitN(s, " ", 3); len(f) < 3 {
src/pkg/net/http/request_test.go
--- a/src/pkg/net/http/request_test.go
+++ b/src/pkg/net/http/request_test.go
@@ -5,6 +5,7 @@
package http_test
import (
+ "bufio"
"bytes"
"fmt"
"io"
@@ -177,6 +178,24 @@ func TestRequestMultipartCallOrder(t *testing.T) {
}
}
+var readRequestErrorTests = []struct {
+ in string
+ err error
+}{
+ {"GET / HTTP/1.1\r\nheader:foo\r\n\r\n", nil},
+ {"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF},
+ {"", io.EOF},
+}
+
+func TestReadRequestErrors(t *testing.T) {
+ for i, tt := range readRequestErrorTests {
+ _, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.in)))
+ if err != tt.err {
+ t.Errorf("%d. got error = %v; want %v", i, err, tt.err)
+ }
+ }
+}
+
func testMissingFile(t *testing.T, req *Request) {
f, fh, err := req.FormFile("missing")
if f != nil {
src/pkg/net/http/server.go
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -601,7 +601,7 @@ func (c *conn) serve() {
// while they're still writing their
// request. Undefined behavior.
msg = "413 Request Entity Too Large"
- } else if err == io.ErrUnexpectedEOF {
+ } else if err == io.EOF {
break // Don't reply
} else if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
break // Don't reply
コアとなるコードの解説
src/pkg/net/http/request.go
の変更
-
変更前:
if s, err = tp.ReadLine(); err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return nil, err }
tp.ReadLine()
がエラーを返した場合、それがio.EOF
であればio.ErrUnexpectedEOF
に変換してからリターンしていました。これにより、リクエストの最初の行を読み込む際にio.EOF
が発生すると、常にio.ErrUnexpectedEOF
が返されていました。 -
変更後:
if s, err = tp.ReadLine(); err != nil { return nil, err } defer func() { if err == io.EOF { err = io.ErrUnexpectedEOF } }()
tp.ReadLine()
がエラーを返した場合、まずそのエラーをそのまま返します。 その直後にdefer
関数が追加されました。このdefer
関数は、ReadRequest
関数が終了する直前に実行されます。defer
関数内で、もしReadRequest
関数全体で発生したエラーがio.EOF
であれば、それをio.ErrUnexpectedEOF
に変換します。 この変更のポイントは、tp.ReadLine()
がio.EOF
を返した直後にはio.EOF
がそのまま返されるようになったことです。io.ErrUnexpectedEOF
への変換は、ReadRequest
関数がリクエストの他の部分(ヘッダやボディ)を読み込んでいる途中でio.EOF
に遭遇した場合にのみ適用されるようになりました。これにより、リクエストの最初の行でEOFが発生した場合と、それ以降でEOFが発生した場合を区別できるようになります。
src/pkg/net/http/request_test.go
の変更
readRequestErrorTests
という新しいテストケースのスライスが追加されました。これには、様々な入力文字列と、それに対応する期待されるエラー(またはエラーなし)が定義されています。"GET / HTTP/1.1\\r\\nheader:foo\\r\\n\\r\\n"
: 正常なリクエスト。期待されるエラーはnil
。"GET / HTTP/1.1\\r\\nheader:foo\\r\\n"
: ヘッダの後に空行がない不完全なリクエスト。期待されるエラーはio.ErrUnexpectedEOF
。これは、ヘッダの読み込み中にEOFに遭遇した場合にio.ErrUnexpectedEOF
が返されることをテストしています。""
: 空の入力。期待されるエラーはio.EOF
。これは、リクエストの最初の行を読み込む前にEOFに遭遇した場合にio.EOF
が返されることをテストしています。
TestReadRequestErrors
関数が追加され、上記のテストケースをループで実行し、ReadRequest
が期待通りのエラーを返すか検証しています。これにより、エラーハンドリングの修正が正しく機能していることを確認できます。
src/pkg/net/http/server.go
の変更
-
変更前:
} else if err == io.ErrUnexpectedEOF { break // Don't reply }
ReadRequest
から返されたエラーがio.ErrUnexpectedEOF
の場合に、クライアントに応答せずに接続を閉じ(break
)ていました。 -
変更後:
} else if err == io.EOF { break // Don't reply }
エラーが
io.EOF
の場合にbreak
するように変更されました。これは、クライアントがリクエストの途中で接続を閉じた(io.EOF
)場合、サーバーは応答すべきではないというHTTPのセマンティクスに合致させるための修正です。以前のio.ErrUnexpectedEOF
のケースは、リクエストボディが大きすぎる場合の413 Request Entity Too Large
と混同される可能性があったため、より正確なio.EOF
に変更されました。
関連リンク
- Go Issue 3298:
net/http
: wrong errors from ReadRequest: https://github.com/golang/go/issues/3298 - Gerrit Change-Id:
Ie8deb3f828886afe3dc7403f128cbafebe9fb1a1
(Go CL 5783080): https://golang.org/cl/5783080
参考にした情報源リンク
- https://github.com/golang/go/issues/3298
- https://golang.org/cl/5783080
- Go言語の
io
パッケージドキュメント:io.EOF
,io.ErrUnexpectedEOF
- Go言語の
net/http
パッケージドキュメント - HTTP/1.1 RFC (特にメッセージフォーマットとエラーハンドリングに関するセクション)