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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける ReadRequest 関数が、HTTPリクエストの読み込み中に適切なエラーを返さない問題を修正するものです。具体的には、io.EOFio.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.goconn.serve() メソッドでは、ReadRequest から返されるエラーに基づいてクライアントへの応答を決定していましたが、ここでも io.ErrUnexpectedEOFio.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 に変換するのは不適切でした。

このコミットでは、以下の修正が行われました。

  1. 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 が発生した場合に限定されるようになりました。

  2. 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 に変更されました。

関連リンク

参考にした情報源リンク