[インデックス 15483] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の transfer.go
ファイルに対する修正です。具体的には、HTTPリクエストのボディを書き込む際の、エラーハンドリングのロジックが改善されています。
コミット
commit 64648986e32ce8d3b1bfdab7fe255c3f69baa163
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date: Wed Feb 27 21:15:36 2013 -0300
net/http: don't drop error on request write
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7230059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64648986e32ce8d3b1bfdab7fe255c3f69baa163
元コミット内容
このコミットは、net/http
パッケージにおいて、HTTPリクエストのボディを書き込む際に発生したエラーが適切に伝播されない問題を修正します。以前の実装では、ボディの書き込み中に発生したエラーが、その後の余分なボディ内容を破棄する処理のエラーによって上書きされ、結果として重要なエラーが無視される可能性がありました。この修正により、ボディ書き込みの主要な処理で発生したエラーが確実に捕捉され、適切に処理されるようになります。
変更の背景
Go言語の net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。HTTPリクエストを送信する際、リクエストボディ(例えばPOSTリクエストのデータ)はネットワークを通じて送信されます。この送信処理中に、ネットワークの問題、ディスクI/Oの問題、またはその他の予期せぬ状況によりエラーが発生する可能性があります。
このコミットが行われる前のコードでは、io.Copy
関数を複数回呼び出してリクエストボディを処理していました。具体的には、まず io.LimitReader
を使って Content-Length
で指定された長さのボディを書き込み、その後、もしボディが Content-Length
よりも長かった場合に、残りのデータを ioutil.Discard
にコピーして破棄していました。
問題は、最初の io.Copy
でエラーが発生した場合でも、そのエラーが err
変数に格納された後、すぐに次の io.Copy(ioutil.Discard, t.Body)
の呼び出しによって err
変数が上書きされてしまう点にありました。これにより、本来処理されるべき重要なエラー(例えば、ネットワーク接続が切断されたことによる書き込みエラー)が失われ、リクエストが成功したかのように誤認される可能性がありました。これは、HTTP通信の信頼性と堅牢性に直接影響するバグでした。
前提知識の解説
- Go言語の
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーを構築するための機能を提供します。Webアプリケーション開発において中心的な役割を担います。 io.Reader
とio.Writer
インターフェース: Go言語におけるI/O操作の基本的なインターフェースです。io.Reader
はデータを読み出すためのRead
メソッドを、io.Writer
はデータを書き込むためのWrite
メソッドを定義します。io.Copy(dst io.Writer, src io.Reader) (written int64, err error)
:src
からデータを読み込み、dst
に書き込むユーティリティ関数です。src
のEOF(End Of File)に到達するか、エラーが発生するまでコピーを続けます。io.LimitReader(r io.Reader, n int64) io.Reader
: 指定されたバイト数n
までしか読み出さないio.Reader
を返します。これは、HTTPのContent-Length
ヘッダで指定されたボディの長さを厳密に守るために使用されます。ioutil.Discard
:io.Writer
インターフェースを実装しており、書き込まれたデータをすべて破棄します。つまり、どこにも書き込まれません。これは、ストリームからデータを読み捨てたい場合(例えば、余分なボディデータを読み飛ばす場合)に便利です。- エラーハンドリング: Go言語では、関数が複数の戻り値を返すことができ、慣習として最後の戻り値にエラーオブジェクトを返します。呼び出し元は、このエラーオブジェクトをチェックして、処理が成功したかどうかを判断します。このコミットの核心は、このエラーオブジェクトの適切な伝播とチェックにあります。
技術的詳細
src/pkg/net/http/transfer.go
ファイル内の transferWriter
型の WriteBody
メソッドは、HTTPリクエストのボディを実際に書き込む役割を担っています。このメソッドは、Content-Length
ヘッダが存在するかどうかに応じて異なるロジックでボディを処理します。
Content-Length
が指定されている場合(t.ContentLength > 0
の条件分岐内)の元のコードは以下のようになっていました。
ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
nextra, err := io.Copy(ioutil.Discard, t.Body) // ここでerrが上書きされる
if err != nil { // ここでチェックされるerrは、2番目のio.Copyからのもの
return err
}
このコードの問題点は、io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
の呼び出しで発生したエラーが、直後の nextra, err := io.Copy(ioutil.Discard, t.Body)
の呼び出しによって err
変数が再宣言され、上書きされてしまうことです。Go言語では、:=
演算子を使って変数を宣言と同時に初期化する場合、同じスコープ内に同名の変数が既に存在しても、新しい変数を宣言してしまいます(ただし、少なくとも1つの新しい変数が宣言される必要があります)。この場合、err
は新しい変数として宣言され、以前の err
の値は失われます。
その結果、if err != nil { return err }
の行でチェックされる err
は、最初の io.Copy
からのものではなく、常に2番目の io.Copy(ioutil.Discard, t.Body)
からのものになっていました。もし最初の io.Copy
でエラーが発生し、かつ2番目の io.Copy
ではエラーが発生しなかった場合、最初の重要なエラーは無視され、関数はエラーなく終了してしまうという誤った挙動を示していました。
このコミットでは、この問題を解決するために、err
変数のスコープとライフサイクルを適切に管理するように修正されました。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -194,10 +194,11 @@ func (t *transferWriter) WriteBody(w io.Writer) (err error) {
tncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
- nextra, err := io.Copy(ioutil.Discard, t.Body)
if err != nil {
return err
}
+ var nextra int64
+ nextra, err = io.Copy(ioutil.Discard, t.Body)
tncopy += nextra
}
if err != nil {
コアとなるコードの解説
修正後のコードは以下のようになります。
ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
if err != nil { // 最初のio.Copyのエラーをここで即座にチェックし、返す
return err
}
var nextra int64 // nextraとerrを個別に宣言
nextra, err = io.Copy(ioutil.Discard, t.Body) // 2番目のio.Copyのエラーは、既存のerr変数に代入
ncopy += nextra
}
if err != nil { // ここでチェックされるerrは、2番目のio.Copyからのもの(もしあれば)
return err
}
変更点は以下の通りです。
ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
の直後に、このio.Copy
から返されたerr
をチェックするif err != nil { return err }
が追加されました。これにより、ボディの主要な書き込み処理でエラーが発生した場合、そのエラーがすぐに捕捉され、関数から返されるようになります。nextra, err := io.Copy(ioutil.Discard, t.Body)
の行が、var nextra int64
とnextra, err = io.Copy(ioutil.Discard, t.Body)
の2行に分割されました。var nextra int64
は、nextra
変数を明示的に宣言します。nextra, err = io.Copy(ioutil.Discard, t.Body)
では、:=
ではなく=
演算子が使用されています。これにより、err
は新しい変数として宣言されるのではなく、関数のシグネチャで既に宣言されている既存のerr
変数(func (t *transferWriter) WriteBody(w io.Writer) (err error)
のerr
)に値が代入されます。
この修正により、最初の io.Copy
で発生したエラーが適切に処理され、その後の io.Copy(ioutil.Discard, t.Body)
のエラーが、最初のエラーを上書きすることなく、既存の err
変数に代入されるようになりました。結果として、WriteBody
関数は、ボディ書き込み中に発生した最も関連性の高いエラーを正確に報告できるようになり、net/http
パッケージの堅牢性が向上しました。
関連リンク
- Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
io/ioutil
パッケージドキュメント (Go 1.16以降はio
パッケージに統合): https://pkg.go.dev/io/ioutil
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/7230059
は、このGerritの変更リストへのリンクです。) - Go言語のエラーハンドリングに関する公式ドキュメントやブログ記事 (一般的なGoのエラーハンドリングのベストプラクティス): https://go.dev/blog/error-handling-and-go
- Go言語の変数宣言と代入に関するドキュメント: https://go.dev/tour/basics/10 (短い変数宣言
:=
と通常の代入=
の違い)