[インデックス 18884] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける、リクエストボディの読み込みエラーが適切に処理されない問題を修正するものです。具体的には、Request.Body
からの読み込み中にエラーが発生した場合に、そのエラーが無視されてしまう挙動を改め、エラーを適切に伝播させるように変更されています。これにより、HTTPリクエストの送信時にボディの読み込みで問題が発生した場合に、クライアント側でそのエラーを検知できるようになります。
コミット
commit 2701dadbac7d3bf166124f19659f9b906a026e0a
Author: Luka Zakrajšek <tr00.g33k@gmail.com>
Date: Mon Mar 17 15:52:52 2014 -0700
net/http: Request Body error should not be ignored.
Fixes #7521.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/76320043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2701dadbac7d3bf166124f19659f9b906a026e0a
元コミット内容
このコミットは、net/http
パッケージにおいて、HTTPリクエストのボディを読み込む際に発生するエラーが無視される問題を修正します。具体的には、Request.ContentLength
が0に設定されているにもかかわらず、実際にはボディが存在し、その読み込み中にエラーが発生した場合に、そのエラーが適切に処理されずに無視されてしまうというバグに対応しています。この修正により、ボディの読み込みエラーが Request
の送信処理に適切に反映されるようになります。
変更の背景
この変更は、Go issue #7521 で報告されたバグを修正するために行われました。
Go issue #7521のタイトルは "net/http: Request Body error should not be ignored." であり、net/http
パッケージにおいて、Request.Body
からの読み込み中に発生するエラーが無視されるという問題が指摘されていました。
具体的には、Request.ContentLength
が0に設定されている場合(これはユーザーが明示的に設定しない限り、ボディの長さが不明であることを示すことが多い)、net/http
パッケージはボディの有無を確認するために少量のデータを読み込もうとします。この読み込み処理中にエラーが発生した場合、以前の実装ではそのエラーが捕捉されず、リクエストの送信処理が続行されてしまう可能性がありました。これにより、クライアントはボディの読み込みに失敗したことを知ることができず、予期せぬ動作やデバッグの困難さを引き起こしていました。
このコミットは、このような状況下で発生するエラーを適切に検出し、リクエストの送信処理に反映させることで、より堅牢なエラーハンドリングを実現することを目的としています。
前提知識の解説
Go言語の net/http
パッケージ
net/http
パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。このパッケージは、HTTP/1.1およびHTTP/2プロトコルをサポートし、リクエストの送信、レスポンスの受信、サーバーの構築など、HTTP通信に関する基本的な機能を提供します。
http.Request
構造体
http.Request
構造体は、HTTPリクエストを表します。この構造体には、リクエストメソッド(GET, POSTなど)、URL、ヘッダー、そしてリクエストボディなどの情報が含まれます。
Body
フィールド:io.ReadCloser
インターフェースを満たす型で、リクエストボディのデータを読み込むためのリーダーです。POSTリクエストなどでデータを送信する際に使用されます。ContentLength
フィールド: リクエストボディの長さをバイト単位で示します。この値が-1の場合、ボディの長さは不明であることを意味し、チャンク転送エンコーディングが使用されることがあります。0の場合、ボディがないことを意味します。
io.Reader
および io.ReadCloser
インターフェース
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースです。このメソッドは、データをp
に読み込み、読み込んだバイト数n
とエラーerr
を返します。データの終端に達した場合はio.EOF
エラーを返します。io.ReadCloser
:io.Reader
とio.Closer
(Close() error
メソッド) を組み合わせたインターフェースです。リソースを読み込んだ後に閉じる必要がある場合に使用されます。http.Request.Body
はこのインターフェースを満たします。
io.ReadFull
関数
io.ReadFull(r Reader, buf []byte) (n int, err error)
は、指定されたリーダー r
から buf
の長さ分のバイトを読み込もうとします。buf
が完全に埋められなかった場合、io.ErrUnexpectedEOF
エラーを返します。読み込み中にエラーが発生した場合はそのエラーを返します。
HTTPリクエストボディの処理
HTTPリクエストにおいて、ボディは通常、POSTやPUTなどのメソッドでデータをサーバーに送信するために使用されます。Content-Length
ヘッダーはボディの長さを明示的に示しますが、これが設定されていない場合や、ContentLength
が0に設定されているにもかかわらずボディが存在する可能性がある場合、net/http
パッケージはボディの有無や実際の長さを判断するために、内部的にボディから少量のデータを読み込もうとすることがあります。
技術的詳細
このコミットの技術的な核心は、net/http/transfer.go
ファイル内の newTransferWriter
関数における Request.Body
の処理ロジックの変更にあります。
以前の実装では、Request.ContentLength
が0の場合、net/http
パッケージはボディの有無を確認するために io.ReadFull(t.Body, buf[:])
を呼び出していました。この際、io.ReadFull
が返すエラーは n, _ := io.ReadFull(t.Body, buf[:])
のようにアンダースコア (_
) で破棄されていました。これは、ボディの有無をチェックする目的であり、読み込みエラー自体は重要視されていなかったためと考えられます。しかし、これにより、t.Body
がエラーを返す io.Reader
であった場合、そのエラーが無視されてしまい、リクエストの送信処理がエラーなしで続行されてしまうという問題がありました。
新しい実装では、この io.ReadFull
の呼び出し結果が n, rerr := io.ReadFull(t.Body, buf[:])
のように rerr
変数で捕捉されるようになりました。そして、rerr != nil && rerr != io.EOF
という条件でエラーがチェックされます。
rerr != nil
: 読み込み中に何らかのエラーが発生したことを示します。rerr != io.EOF
: データの終端に達したことによる正常な終了 (io.EOF
) ではないことを示します。
この条件が真の場合、つまり io.EOF
以外のエラーが発生した場合、以下の処理が行われます。
t.ContentLength = -1
: リクエストのContentLength
を-1に設定します。これは、ボディの長さが不明であり、エラーが発生したことを示唆します。t.Body = &errorReader{rerr}
: 元のt.Body
を、新しく定義されたerrorReader
型のインスタンスに置き換えます。このerrorReader
は、コンストラクタで受け取ったエラーを常に返すio.Reader
です。これにより、後続のボディ読み込み処理がこのerrorReader
から読み込もうとすると、元のエラーが再発生し、適切に伝播されるようになります。
また、この修正を検証するために、src/pkg/net/http/requestwrite_test.go
に新しいテストケースが追加されています。これらのテストケースは、ContentLength
が0に設定されたリクエストで、Body
が読み込み時にエラーを返すようなシナリオをシミュレートし、期待されるエラーが適切に返されることを確認しています。
errorReader
構造体
コミットによって src/pkg/net/http/transfer.go
に追加された errorReader
構造体は、この修正の重要な要素です。
type errorReader struct {
err error
}
func (r *errorReader) Read(p []byte) (n int, err error) {
return 0, r.err
}
この構造体は io.Reader
インターフェースを満たしており、その Read
メソッドは常に 0
バイトを読み込んだことと、コンストラクタで渡されたエラー (r.err
) を返します。これにより、一度ボディの読み込みでエラーが発生した場合、その後のボディ読み込み試行はすべて同じエラーを返すようになり、エラーが適切に伝播されるメカニズムを提供します。
コアとなるコードの変更箇所
src/pkg/net/http/requestwrite_test.go
このファイルには、新しいテストケースが追加されています。これらのテストケースは、ContentLength
が0のリクエストボディがエラーを返すシナリオを検証します。
Request with a 0 ContentLength and a body with 1 byte content and an error.
- ボディが1バイトのデータとそれに続くエラーを返す場合。
Request with a 0 ContentLength and a body without content and an error.
- ボディがコンテンツを持たず、直接エラーを返す場合。
これらのテストは、io.MultiReader
とカスタムの errorReader
を組み合わせて、特定のタイミングでエラーを発生させるボディを作成し、http.Request
の書き込み時に期待されるエラーが返されることを確認しています。
src/pkg/net/http/transfer.go
このファイルには、newTransferWriter
関数内のロジックと、新しい errorReader
構造体が追加されています。
errorReader
構造体の追加: 前述の通り、エラーを常に返すio.Reader
の実装です。newTransferWriter
関数内の変更:
この変更により、--- a/src/pkg/net/http/transfer.go +++ b/src/pkg/net/http/transfer.go @@ -53,8 +61,11 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) { if t.ContentLength == 0 { // Test to see if it's actually zero or just unset. var buf [1]byte - n, _ := io.ReadFull(t.Body, buf[:]) - if n == 1 { + n, rerr := io.ReadFull(t.Body, buf[:]) + if rerr != nil && rerr != io.EOF { + t.ContentLength = -1 + t.Body = &errorReader{rerr} + } else if n == 1 { // Oh, guess there is data in this Body Reader after all. // The ContentLength field just wasn't set. // Stich the Body back together again, re-attaching our
io.ReadFull
の戻り値であるエラーがrerr
として捕捉され、io.EOF
以外のエラーが発生した場合にContentLength
を-1に設定し、Body
をerrorReader
に置き換える処理が追加されました。
コアとなるコードの解説
このコミットの核心は、net/http/transfer.go
の newTransferWriter
関数における Request.Body
の初期チェックロジックの改善です。
newTransferWriter
関数は、HTTPリクエストまたはレスポンスをワイヤーフォーマットで書き込むための transferWriter
を初期化する役割を担っています。この関数内で、リクエストボディの ContentLength
が0に設定されている場合、システムは実際にボディが存在しないのか、それとも単に ContentLength
が設定されていないだけでボディにデータがあるのかを判断しようとします。
以前のコードでは、この判断のために io.ReadFull(t.Body, buf[:])
を呼び出し、1バイトのデータを読み込もうとしていました。この際、読み込み中に発生したエラーは _
で破棄されていました。これは、ボディの有無の確認が主目的であり、読み込みエラー自体は無視されていたためです。
しかし、この挙動は、t.Body
がエラーを返す io.Reader
であった場合に問題を引き起こしました。例えば、ネットワークエラーやディスクI/Oエラーなどにより、ボディの読み込みが途中で失敗した場合でも、そのエラーが無視され、リクエストの送信処理が続行されてしまう可能性がありました。これにより、クライアントはボディの読み込みエラーを検知できず、サーバー側で予期せぬ動作が発生したり、デバッグが困難になったりする可能性がありました。
新しいコードでは、io.ReadFull
の戻り値であるエラーが rerr
変数に明示的に捕捉されます。そして、if rerr != nil && rerr != io.EOF
という条件で、io.EOF
以外のエラーが発生したかどうかがチェックされます。
rerr != nil
:io.ReadFull
が何らかのエラーを返したことを意味します。rerr != io.EOF
: 読み込みがファイルの終端に達したことによる正常な終了 (io.EOF
) ではないことを意味します。つまり、予期せぬエラーが発生したことを示します。
この条件が真の場合、つまりボディの読み込み中に io.EOF
以外のエラーが発生した場合、以下の重要な処理が行われます。
t.ContentLength = -1
:transferWriter
のContentLength
を-1
に設定します。これは、ボディの長さが不明であり、かつ読み込み中にエラーが発生したことを示すシグナルとなります。これにより、後続の処理でボディの長さが不明であること、およびエラーが発生したことが適切に認識されるようになります。t.Body = &errorReader{rerr}
: 元のt.Body
を、新しく定義されたerrorReader
のインスタンスに置き換えます。errorReader
は、そのRead
メソッドが常にコンストラクタで渡されたエラー (rerr
) を返すカスタムio.Reader
です。この置き換えにより、その後のtransferWriter
がt.Body
からデータを読み込もうとするたびに、最初に発生したエラーが再発生し、最終的にリクエストの送信処理全体にエラーが伝播されるようになります。
この修正により、net/http
クライアントは、リクエストボディの読み込み中に発生したエラーを確実に検知し、適切に処理できるようになります。これは、HTTP通信の堅牢性と信頼性を向上させる上で非常に重要な変更です。
関連リンク
- Go issue #7521: https://github.com/golang/go/issues/7521
- Go CL 76320043: https://golang.org/cl/76320043
参考にした情報源リンク
- Go issue #7521 の内容
- Go CL 76320043 のコードレビュー
- Go言語の
net/http
パッケージのドキュメント - Go言語の
io
パッケージのドキュメント - HTTP/1.1 仕様 (RFC 2616) - 特に
Content-Length
ヘッダーとメッセージボディに関するセクション