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

[インデックス 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.Readerio.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
		}

変更点は以下の通りです。

  1. ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength)) の直後に、この io.Copy から返された err をチェックする if err != nil { return err } が追加されました。これにより、ボディの主要な書き込み処理でエラーが発生した場合、そのエラーがすぐに捕捉され、関数から返されるようになります。
  2. nextra, err := io.Copy(ioutil.Discard, t.Body) の行が、var nextra int64nextra, 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 パッケージの堅牢性が向上しました。

関連リンク

参考にした情報源リンク