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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける HEAD リクエストの処理に関するバグ修正と、関連するテストの改善を目的としています。具体的には、ReadResponse 関数と (*http.Response).Write メソッドが HEAD リクエストに対して誤った振る舞いをすること、およびチャンクエンコーディングされたレスポンスのテストケースに不備があった点を修正しています。

コミット

commit 087b708fd3c60a2ca753738c1cc8072978493a3a
Author: John Graham-Cumming <jgc@jgc.org>
Date:   Thu Feb 28 09:29:50 2013 -0800

    net/http: fix handling of HEAD in ReadResponse and (*http.Response).Write
    
    The test suite for ReadResponse was not checking the error return on the io.Copy
    on the body. This was masking two errors: the handling of chunked responses to
    HEAD requests and the handling of Content-Length > 0 to HEAD.
    
    The former manifested itself as an 'unexpected EOF' when doing the io.Copy
    because a chunked reader was assigned but there were no chunks to read. The
    latter cause (*http.Response).Write to report an error on HEAD requests
    because it saw a Content-Length > 0 and expected a body.
    
    There was also a missing \r\n in one chunked test that meant that the chunked
    encoding was malformed. This does not appear to have been intentional.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7407046

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

https://github.com/golang/go/commit/087b708fd3c60a2ca753738c1cc8072978493a3a

元コミット内容

net/http: ReadResponse および (*http.Response).Write における HEAD の処理を修正

ReadResponse のテストスイートが、ボディに対する io.Copy のエラー戻り値をチェックしていなかった。これにより、以下の2つのエラーが隠蔽されていた。

  1. HEAD リクエストに対するチャンクエンコーディングされたレスポンスの処理
  2. HEAD リクエストに対する Content-Length > 0 の処理

前者は、チャンクリーダーが割り当てられたにもかかわらず読み取るチャンクが存在しないため、io.Copy 実行時に「unexpected EOF」として現れた。後者は、(*http.Response).WriteContent-Length > 0 を見てボディを期待したため、HEAD リクエストでエラーを報告した。

また、チャンクテストの1つに \r\n が欠落しており、チャンクエンコーディングが不正になっていた。これは意図的なものではないと思われる。

変更の背景

このコミットは、Go言語の net/http パッケージがHTTPプロトコルの HEAD メソッドを正しく処理できていなかったという問題に対処するために行われました。HEAD メソッドは、GET メソッドと同様のヘッダーを返しますが、レスポンスボディを含みません。しかし、Content-Length ヘッダーは GET リクエストで返されるであろうボディの長さを引き続き示すことができます。

既存のテストスイートでは、ReadResponse がレスポンスボディを読み込む際に io.Copy のエラーを適切にチェックしていなかったため、この HEAD リクエストに関する不正確な振る舞いが表面化していませんでした。具体的には、以下の2つのシナリオで問題が発生していました。

  1. チャンクエンコーディングされた HEAD レスポンス: HEAD リクエストに対してチャンクエンコーディングが指定された場合、net/http はチャンクリーダーを割り当てますが、実際にはボディが存在しないため、io.Copy がボディを読み込もうとすると「unexpected EOF」エラーが発生していました。
  2. Content-Length > 0 を持つ HEAD レスポンス: HEAD リクエストのレスポンスヘッダーに Content-Length: N (N > 0) が含まれている場合、(*http.Response).Write メソッドはボディが存在すると誤解し、ボディが書き込まれないためにエラーを報告していました。

さらに、既存のチャンクエンコーディングのテストケース自体にも、HTTPプロトコル仕様に準拠しない \r\n の欠落という軽微なバグがあり、これも修正の対象となりました。これらの問題は、net/http パッケージの堅牢性とHTTPプロトコル準拠の観点から修正が必要でした。

前提知識の解説

HTTP HEAD メソッド

HTTP HEAD メソッドは、GET メソッドと全く同じヘッダーを返しますが、レスポンスボディは返しません。これは、リソースのメタデータ(例えば、Content-TypeContent-LengthLast-Modified など)を取得したいが、実際のコンテンツは必要ない場合に非常に役立ちます。例えば、ファイルが存在するかどうかを確認したり、ファイルのサイズや最終更新日時を取得したりするのに使われます。

重要な点は、HEAD レスポンスにはボディがないにもかかわらず、Content-Length ヘッダーが存在しうるということです。この Content-Length は、もし同じリソースに対して GET リクエストが行われた場合に返されるであろうボディの長さを表します。

HTTP チャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1 では、レスポンスボディの長さを事前に知ることができない場合に、チャンク転送エンコーディングを使用できます。これは、ボディを複数の「チャンク」に分割して送信する方法です。各チャンクは、そのチャンクのサイズ(16進数)と、それに続くデータ、そして \r\n で構成されます。最後のチャンクはサイズが 0 で、その後に \r\n が2回続きます (0\r\n\r\n)。これにより、受信側はボディの終わりを認識できます。

HEAD リクエストの場合、ボディは存在しませんが、プロトコル上はチャンクエンコーディングが指定される可能性があります。この場合、ボディは 0\r\n\r\n の終端チャンクのみで構成されるべきです。

io.Copy とエラーハンドリング

Go言語の io.Copy(dst, src) 関数は、src から dst へデータをコピーします。この関数は、コピーされたバイト数と、コピー中に発生したエラーを返します。ネットワークI/Oでは、予期せぬ接続切断やデータ不足など、様々なエラーが発生する可能性があります。そのため、io.Copy の戻り値であるエラーを適切にチェックすることは、堅牢なプログラムを書く上で不可欠です。エラーを無視すると、今回のように潜在的なバグが隠蔽される可能性があります。

net/http パッケージの内部構造 (簡略化)

  • ReadResponse: クライアントからのリクエストやサーバーからのレスポンスを読み込み、*http.Response 構造体にパースする役割を担います。この過程で、レスポンスヘッダーに基づいてボディの読み込み方法(例: Content-Length に基づく読み込み、チャンク読み込み)を決定し、適切な io.ReaderResponse.Body フィールドに設定します。
  • (*http.Response).Write: *http.Response 構造体の内容(ステータスライン、ヘッダー、ボディ)を io.Writer に書き込み、HTTPレスポンスとして整形する役割を担います。この際、Content-LengthTransfer-Encoding ヘッダーに基づいてボディの書き込み方法を調整します。

技術的詳細

このコミットは、主に以下の3つの技術的な修正を含んでいます。

  1. io.Copy のエラーチェックの追加: src/pkg/net/http/response_test.goTestReadResponse 関数において、レスポンスボディを読み込む io.Copy(&bout, rbody) の呼び出しが、その戻り値であるエラーをチェックしていませんでした。この修正では、_, err = io.Copy(&bout, rbody) と変更し、if err != nil ブロックを追加してエラーを捕捉し、テストを失敗させるようにしました。これにより、これまで隠蔽されていた HEAD リクエストに関する問題が表面化するようになりました。

  2. HEAD リクエストに対する Content-Length の処理の修正: src/pkg/net/http/transfer.gotransferWriter.WriteBody メソッドにおいて、Content-Length と実際に書き込まれたバイト数 ncopy が一致しない場合にエラーを報告するロジックがありました。しかし、HEAD リクエストの場合、Content-Length は存在するがボディは存在しないため、このチェックは常にエラーを引き起こしていました。 修正では、条件式に !t.ResponseToHEAD を追加しました。これにより、レスポンスが HEAD リクエストに対するものである場合、この Content-Length とボディ長の不一致チェックをスキップするようになります。t.ResponseToHEAD は、内部的にリクエストメソッドが HEAD であるかどうかを示すフラグです。

  3. チャンクエンコーディングされた HEAD レスポンスの処理の修正: src/pkg/net/http/transfer.goreadTransfer 関数において、チャンクエンコーディングが指定された場合のボディリーダーの割り当てロジックが修正されました。 以前は、チャンクエンコーディングの場合、常に newChunkedReader(r) をボディリーダーとして割り当てていました。しかし、HEAD リクエストのようにボディが期待されない場合、このチャンクリーダーは読み取るべきチャンクがないため、io.Copyunexpected EOF を発生させていました。 修正では、noBodyExpected(t.RequestMethod) というヘルパー関数(内部的には HEADCONNECT メソッドなどをチェック)を導入し、もしボディが期待されないリクエストメソッドであれば、io.LimitReader(r, 0) をボディリーダーとして割り当てます。io.LimitReader(r, 0) は、基になるリーダー r から0バイトしか読み込まないリーダーであり、すぐに EOF を返します。これにより、HEAD リクエストに対するチャンクエンコーディングのレスポンスで、ボディを読み込もうとした際に unexpected EOF が発生するのを防ぎます。

  4. チャンクテストの修正: src/pkg/net/http/response_test.gorespTests 変数内のチャンクエンコーディングのテストケースにおいて、チャンクの終端を示す \r\n が1つ欠落していました。 "Body here\\n"\"Body here\\n\\r\\n\" に修正され、HTTPチャンクエンコーディングの仕様に準拠するようになりました。これにより、テストケース自体が正しくなり、より正確なテストが可能になりました。

これらの修正により、net/http パッケージは HEAD リクエストをHTTPプロトコル仕様に則って正しく処理できるようになり、堅牢性が向上しました。

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

src/pkg/net/http/response_test.go

--- a/src/pkg/net/http/response_test.go
+++ b/src/pkg/net/http/response_test.go
@@ -157,7 +157,7 @@ var respTests = []respTest{
 			"Content-Length: 10\r\n" +\
 			"\r\n" +\
 			"0a\r\n" +\
-"\t\t\t\"Body here\\n\" +\
+"\t\t\t\"Body here\\n\\r\\n\" +\
 			"0\r\n" +\
 			"\r\n",
 
@@ -327,13 +327,10 @@ var respTests = []respTest{
 }\
 
 func TestReadResponse(t *testing.T) {
-	for i := range respTests {
-		tt := &respTests[i]
-		var braw bytes.Buffer
-		braw.WriteString(tt.Raw)
-		resp, err := ReadResponse(bufio.NewReader(&braw), tt.Resp.Request)
+	for i, tt := range respTests {
+		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
 		if err != nil {
-"\t\t\tt.Errorf(\"#%d: %s\", i, err)\
+"\t\t\tt.Errorf(\"#%d: %v\", i, err)\
 			continue
 		}\
 		rbody := resp.Body
@@ -341,7 +338,11 @@ func TestReadResponse(t *testing.T) {
 		diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp)\
 		var bout bytes.Buffer
 		if rbody != nil {
-"\t\t\tio.Copy(&bout, rbody)\
+"\t\t\t_, err = io.Copy(&bout, rbody)\
+"\t\t\tif err != nil {\
+"\t\t\t\tt.Errorf(\"#%d: %v\", i, err)\
+"\t\t\t\tcontinue\
+"\t\t\t}\
 			rbody.Close()\
 		}\
 		body := bout.String()\
@@ -351,6 +352,22 @@ func TestReadResponse(t *testing.T) {
 	}\
 }\
 
+func TestWriteResponse(t *testing.T) {
+	for i, tt := range respTests {
+		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
+		if err != nil {
+			t.Errorf("#%d: %v", i, err)
+			continue
+		}
+		bout := bytes.NewBuffer(nil)
+		err = resp.Write(bout)
+		if err != nil {
+			t.Errorf("#%d: %v", i, err)
+			continue
+		}
+	}
+}
+
 var readResponseCloseInMiddleTests = []struct {
 	chunked, compressed bool
 }{\

src/pkg/net/http/transfer.go

--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -209,7 +209,7 @@ func (t *transferWriter) WriteBody(w io.Writer) (err error) {
 		}\
 	}\
 
-"\tif t.ContentLength != -1 && t.ContentLength != ncopy {\
+"\tif !t.ResponseToHEAD && t.ContentLength != -1 && t.ContentLength != ncopy {\
 		return fmt.Errorf(\"http: Request.ContentLength=%d with Body length %d\",\
 			t.ContentLength, ncopy)\
 	}\
@@ -327,7 +327,11 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 	// or close connection when finished, since multipart is not supported yet
 	switch {\
 	case chunked(t.TransferEncoding):\
-"\t\tt.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}\
+"\t\tif noBodyExpected(t.RequestMethod) {\
+"\t\t\tt.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}\
+"\t\t} else {\
+"\t\t\tt.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}\
+"\t\t}\
 	case realLength >= 0:\
 	\t// TODO: limit the Content-Length. This is an easy DoS vector.\
 	\tt.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}\

コアとなるコードの解説

src/pkg/net/http/response_test.go の変更点

  • チャンクテストの修正 (respTests): "Body here\\n" の後に \\r\\n が追加されました。これは、HTTPチャンクエンコーディングの仕様において、各チャンクのデータ部分の後に CRLF (\r\n) が必要であるためです。この修正により、テストケースがプロトコルに厳密に準拠するようになりました。

  • TestReadResponse のエラーハンドリング強化: 以前は io.Copy(&bout, rbody) の戻り値であるエラーが無視されていました。修正後、_, err = io.Copy(&bout, rbody) と変更され、errnil でない場合に t.Errorf を呼び出してテストを失敗させるようになりました。これにより、ReadResponse がボディを読み込む際に発生する可能性のあるエラー(特に HEAD リクエストでの unexpected EOF)が適切に捕捉され、テストで検出できるようになりました。

  • TestWriteResponse の追加: (*http.Response).Write メソッドの動作を検証するための新しいテスト関数が追加されました。このテストは、ReadResponse で読み込んだレスポンスを再度 Write メソッドで書き出すことで、Content-Length > 0 を持つ HEAD レスポンスが正しく処理されるかを確認します。このテストの追加は、(*http.Response).Write のバグ修正を検証するために不可欠です。

src/pkg/net/http/transfer.go の変更点

  • transferWriter.WriteBody における HEAD リクエストの考慮: Content-Length と実際に書き込まれたバイト数 ncopy の不一致をチェックする条件式が !t.ResponseToHEAD && t.ContentLength != -1 && t.ContentLength != ncopy に変更されました。 !t.ResponseToHEAD が追加されたことで、もしレスポンスが HEAD リクエストに対するものであれば、このチェックはスキップされます。HEAD リクエストはボディを持たないため、Content-Length が存在しても ncopy は常に0になります。この修正により、HEAD レスポンスで Content-Length が指定されていても、(*http.Response).Write が誤ってエラーを報告することがなくなりました。

  • readTransfer におけるチャンクエンコーディングと HEAD リクエストの統合: チャンクエンコーディングされたレスポンスのボディリーダーを割り当てるロジックが変更されました。

    if noBodyExpected(t.RequestMethod) {
        t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}
    } else {
        t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
    }
    

    noBodyExpected(t.RequestMethod) は、現在のリクエストメソッド(t.RequestMethod)がボディを期待しないもの(例: HEADCONNECT)であるかを判断する内部ヘルパー関数です。 もしボディが期待されない場合、io.LimitReader(r, 0) がボディリーダーとして設定されます。このリーダーは、基になる bufio.Reader r から0バイトしか読み込まないため、すぐに EOF を返します。これにより、HEAD リクエストに対してチャンクエンコーディングが指定されていても、ボディを読み込もうとした際に unexpected EOF エラーが発生するのを防ぎます。 それ以外の場合(ボディが期待される場合)は、これまで通り newChunkedReader(r) が使用されます。

これらの変更は、HTTPプロトコルのセマンティクス、特に HEAD メソッドの特性を正確に反映し、net/http パッケージの堅牢性と正確性を向上させています。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • RFC 2616 (HTTP/1.1)
  • Goのソースコードリポジトリ (GitHub)
  • Goのコードレビューシステム (golang.org/cl)
  • Stack Overflowなどの技術Q&Aサイト (一般的なHTTPプロトコルやGoのI/Oに関する知識)