[インデックス 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つのエラーが隠蔽されていた。
HEAD
リクエストに対するチャンクエンコーディングされたレスポンスの処理HEAD
リクエストに対するContent-Length > 0
の処理
前者は、チャンクリーダーが割り当てられたにもかかわらず読み取るチャンクが存在しないため、io.Copy
実行時に「unexpected EOF」として現れた。後者は、(*http.Response).Write
が Content-Length > 0
を見てボディを期待したため、HEAD
リクエストでエラーを報告した。
また、チャンクテストの1つに \r\n
が欠落しており、チャンクエンコーディングが不正になっていた。これは意図的なものではないと思われる。
変更の背景
このコミットは、Go言語の net/http
パッケージがHTTPプロトコルの HEAD
メソッドを正しく処理できていなかったという問題に対処するために行われました。HEAD
メソッドは、GET
メソッドと同様のヘッダーを返しますが、レスポンスボディを含みません。しかし、Content-Length
ヘッダーは GET
リクエストで返されるであろうボディの長さを引き続き示すことができます。
既存のテストスイートでは、ReadResponse
がレスポンスボディを読み込む際に io.Copy
のエラーを適切にチェックしていなかったため、この HEAD
リクエストに関する不正確な振る舞いが表面化していませんでした。具体的には、以下の2つのシナリオで問題が発生していました。
- チャンクエンコーディングされた
HEAD
レスポンス:HEAD
リクエストに対してチャンクエンコーディングが指定された場合、net/http
はチャンクリーダーを割り当てますが、実際にはボディが存在しないため、io.Copy
がボディを読み込もうとすると「unexpected EOF」エラーが発生していました。 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-Type
、Content-Length
、Last-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.Reader
をResponse.Body
フィールドに設定します。(*http.Response).Write
:*http.Response
構造体の内容(ステータスライン、ヘッダー、ボディ)をio.Writer
に書き込み、HTTPレスポンスとして整形する役割を担います。この際、Content-Length
やTransfer-Encoding
ヘッダーに基づいてボディの書き込み方法を調整します。
技術的詳細
このコミットは、主に以下の3つの技術的な修正を含んでいます。
-
io.Copy
のエラーチェックの追加:src/pkg/net/http/response_test.go
のTestReadResponse
関数において、レスポンスボディを読み込むio.Copy(&bout, rbody)
の呼び出しが、その戻り値であるエラーをチェックしていませんでした。この修正では、_, err = io.Copy(&bout, rbody)
と変更し、if err != nil
ブロックを追加してエラーを捕捉し、テストを失敗させるようにしました。これにより、これまで隠蔽されていたHEAD
リクエストに関する問題が表面化するようになりました。 -
HEAD
リクエストに対するContent-Length
の処理の修正:src/pkg/net/http/transfer.go
のtransferWriter.WriteBody
メソッドにおいて、Content-Length
と実際に書き込まれたバイト数ncopy
が一致しない場合にエラーを報告するロジックがありました。しかし、HEAD
リクエストの場合、Content-Length
は存在するがボディは存在しないため、このチェックは常にエラーを引き起こしていました。 修正では、条件式に!t.ResponseToHEAD
を追加しました。これにより、レスポンスがHEAD
リクエストに対するものである場合、このContent-Length
とボディ長の不一致チェックをスキップするようになります。t.ResponseToHEAD
は、内部的にリクエストメソッドがHEAD
であるかどうかを示すフラグです。 -
チャンクエンコーディングされた
HEAD
レスポンスの処理の修正:src/pkg/net/http/transfer.go
のreadTransfer
関数において、チャンクエンコーディングが指定された場合のボディリーダーの割り当てロジックが修正されました。 以前は、チャンクエンコーディングの場合、常にnewChunkedReader(r)
をボディリーダーとして割り当てていました。しかし、HEAD
リクエストのようにボディが期待されない場合、このチャンクリーダーは読み取るべきチャンクがないため、io.Copy
がunexpected EOF
を発生させていました。 修正では、noBodyExpected(t.RequestMethod)
というヘルパー関数(内部的にはHEAD
やCONNECT
メソッドなどをチェック)を導入し、もしボディが期待されないリクエストメソッドであれば、io.LimitReader(r, 0)
をボディリーダーとして割り当てます。io.LimitReader(r, 0)
は、基になるリーダーr
から0バイトしか読み込まないリーダーであり、すぐにEOF
を返します。これにより、HEAD
リクエストに対するチャンクエンコーディングのレスポンスで、ボディを読み込もうとした際にunexpected EOF
が発生するのを防ぎます。 -
チャンクテストの修正:
src/pkg/net/http/response_test.go
のrespTests
変数内のチャンクエンコーディングのテストケースにおいて、チャンクの終端を示す\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)
と変更され、err
がnil
でない場合に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
)がボディを期待しないもの(例:HEAD
、CONNECT
)であるかを判断する内部ヘルパー関数です。 もしボディが期待されない場合、io.LimitReader(r, 0)
がボディリーダーとして設定されます。このリーダーは、基になるbufio.Reader
r
から0バイトしか読み込まないため、すぐにEOF
を返します。これにより、HEAD
リクエストに対してチャンクエンコーディングが指定されていても、ボディを読み込もうとした際にunexpected EOF
エラーが発生するのを防ぎます。 それ以外の場合(ボディが期待される場合)は、これまで通りnewChunkedReader(r)
が使用されます。
これらの変更は、HTTPプロトコルのセマンティクス、特に HEAD
メソッドの特性を正確に反映し、net/http
パッケージの堅牢性と正確性を向上させています。
関連リンク
- HTTP/1.1 仕様 (RFC 2616) - HEAD メソッド: https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3
- HTTP/1.1 仕様 (RFC 2616) - チャンク転送エンコーディング: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
- Go言語
io
パッケージ: https://pkg.go.dev/io - Go言語
net/http
パッケージ: https://pkg.go.dev/net/http
参考にした情報源リンク
- Goの公式ドキュメント
- RFC 2616 (HTTP/1.1)
- Goのソースコードリポジトリ (GitHub)
- Goのコードレビューシステム (golang.org/cl)
- Stack Overflowなどの技術Q&Aサイト (一般的なHTTPプロトコルやGoのI/Oに関する知識)