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

[インデックス 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.Readerio.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 以外のエラーが発生した場合、以下の処理が行われます。

  1. t.ContentLength = -1: リクエストの ContentLength を-1に設定します。これは、ボディの長さが不明であり、エラーが発生したことを示唆します。
  2. 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に設定し、BodyerrorReader に置き換える処理が追加されました。

コアとなるコードの解説

このコミットの核心は、net/http/transfer.gonewTransferWriter 関数における 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 以外のエラーが発生した場合、以下の重要な処理が行われます。

  1. t.ContentLength = -1: transferWriterContentLength-1 に設定します。これは、ボディの長さが不明であり、かつ読み込み中にエラーが発生したことを示すシグナルとなります。これにより、後続の処理でボディの長さが不明であること、およびエラーが発生したことが適切に認識されるようになります。
  2. t.Body = &errorReader{rerr}: 元の t.Body を、新しく定義された errorReader のインスタンスに置き換えます。errorReader は、その Read メソッドが常にコンストラクタで渡されたエラー (rerr) を返すカスタム io.Reader です。この置き換えにより、その後の transferWritert.Body からデータを読み込もうとするたびに、最初に発生したエラーが再発生し、最終的にリクエストの送信処理全体にエラーが伝播されるようになります。

この修正により、net/http クライアントは、リクエストボディの読み込み中に発生したエラーを確実に検知し、適切に処理できるようになります。これは、HTTP通信の堅牢性と信頼性を向上させる上で非常に重要な変更です。

関連リンク

参考にした情報源リンク

  • Go issue #7521 の内容
  • Go CL 76320043 のコードレビュー
  • Go言語の net/http パッケージのドキュメント
  • Go言語の io パッケージのドキュメント
  • HTTP/1.1 仕様 (RFC 2616) - 特に Content-Length ヘッダーとメッセージボディに関するセクション