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

[インデックス 10786] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の transport.go ファイルにおけるバグ修正です。具体的には、HTTPレスポンスのgzip解凍処理におけるエラーチェックの誤りを修正しています。

コミット

net/http: fix bug in error checking

Thanks to josef86@gmail.com for pointing this out.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/5477092

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c0421d92c87c9428a5f3f8e5457fda88db585c90

元コミット内容

net/http パッケージにおいて、エラーチェックのバグを修正します。この問題は josef86@gmail.com によって指摘されました。

変更の背景

このコミットの背景には、Go言語の net/http パッケージがHTTPクライアントとして動作する際に、受信したHTTPレスポンスのボディがgzip圧縮されている場合にそれを透過的に解凍する機能があります。この解凍処理において、gzip.NewReader 関数がエラーを返した場合に、そのエラーが正しくハンドリングされていないというバグが存在していました。

具体的には、gzip.NewReader の呼び出し結果として返されるエラー変数 (zerr) ではなく、スコープ内の別の err 変数を誤ってチェックしていたため、gzip解凍に失敗してもそのエラーが適切に検出されず、予期せぬ動作やクラッシュにつながる可能性がありました。この問題は外部からの指摘(josef86@gmail.com)によって発見され、修正の必要性が認識されました。

前提知識の解説

Go言語のエラーハンドリング

Go言語では、エラーは関数の戻り値として明示的に返されます。慣例として、エラーは常に最後の戻り値として error 型で返され、エラーがない場合は nil が返されます。開発者は、関数呼び出しの直後にこのエラー戻り値をチェックし、if err != nil のような形でエラーの有無を判断し、適切なエラー処理を行うことが期待されます。

result, err := someFunction()
if err != nil {
    // エラー処理
}
// 正常処理

net/http パッケージ

net/http はGo言語の標準ライブラリであり、HTTPクライアントとサーバーの実装を提供します。このパッケージは、Webアプリケーションの構築やHTTPリクエストの送信など、ネットワーク通信の多くの側面を扱います。

http.TransportpersistConn

http.Transport は、HTTPリクエストのラウンドトリップ(リクエストの送信からレスポンスの受信まで)を実装する構造体です。これには、接続の再利用、プロキシのサポート、TLS設定などが含まれます。 persistConnhttp.Transport の内部で使用される構造体で、単一の持続的な(Keep-Alive)HTTP接続を管理します。この構造体内の readLoop メソッドは、サーバーからのレスポンスを非同期的に読み取る役割を担います。

compress/gzip パッケージ

compress/gzip はGo言語の標準ライブラリで、gzip形式の圧縮データと非圧縮データの間の変換をサポートします。 gzip.NewReader(io.Reader) 関数は、io.Reader インターフェースを実装する入力ストリーム(この場合はHTTPレスポンスボディ)を受け取り、そのストリームからgzip圧縮されたデータを読み込み、非圧縮データとして提供する新しい io.Reader を返します。この関数は、リーダーの作成に失敗した場合(例:入力データが有効なgzip形式でない場合)にエラーを返す可能性があります。

技術的詳細

このバグは、Go言語におけるエラー変数のスコープとシャドーイング(shadowing)に関する一般的な落とし穴を示しています。

問題のコードは src/pkg/net/http/transport.goreadLoop 関数内にありました。この関数は、HTTPレスポンスを読み取り、必要に応じてgzip解凍を処理します。

元のコードスニペットは以下のようでした。

gzReader, zerr := gzip.NewReader(resp.Body)
if err != nil { // ここで誤った変数 'err' をチェックしていた
    pc.close()
    err = zerr // ここで 'err' に 'zerr' を代入しているが、チェックが手遅れ
} else {
    // 正常なgzipリーダーの処理
}

ここで重要なのは、gzip.NewReader(resp.Body) の呼び出しです。この関数は2つの戻り値を返します。1つは *gzip.Reader 型のリーダー、もう1つは error 型のエラーです。これらの戻り値は、gzReader, zerr := ... という形で新しい変数 gzReaderzerr に代入されています。

問題は、その直後の if err != nil という条件文にありました。この err 変数は、gzip.NewReader の呼び出しによって返された zerr とは異なる、より外側のスコープで宣言された別の err 変数でした。したがって、gzip.NewReader がエラー (zerrnil でない) を返しても、if err != nil の条件は真にならず、エラーが適切に検出されませんでした。結果として、無効なgzipデータが渡された場合でも、エラー処理ロジックがスキップされ、後続の処理でパニックや不正なデータ処理が発生する可能性がありました。

修正は非常にシンプルで、gzip.NewReader が返したエラー変数 zerr を直接チェックするように変更されました。

gzReader, zerr := gzip.NewReader(resp.Body)
if zerr != nil { // 正しく 'zerr' をチェックするようになった
    pc.close()
    err = zerr
} else {
    // 正常なgzipリーダーの処理
}

この変更により、gzip.NewReader がエラーを返した場合、そのエラーが即座に捕捉され、pc.close() を呼び出して接続を閉じ、err 変数に zerr を代入することで、readLoop 関数全体の適切なエラーパスに沿って処理が継続されるようになりました。これは、Go言語における正確なエラーハンドリングの重要性を示す典型的な例です。

コアとなるコードの変更箇所

変更は src/pkg/net/http/transport.go ファイルの以下の1行です。

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -544,7 +544,7 @@ func (pc *persistConn) readLoop() {
 				resp.Header.Del("Content-Length")
 				resp.ContentLength = -1
 				gzReader, zerr := gzip.NewReader(resp.Body)
-				if err != nil {
+				if zerr != nil {
 					pc.close()
 					err = zerr
 				} else {

具体的には、547行目の if err != nil {if zerr != nil { に変更されました。

コアとなるコードの解説

persistConn.readLoop() 関数は、持続的なHTTP接続上でレスポンスを読み取るためのゴルーチン内で実行されます。この関数は、受信したレスポンスのヘッダーを解析し、必要に応じてボディの処理を行います。

変更されたコードブロックは、レスポンスがgzip圧縮されている場合にそのボディを解凍する部分です。

  1. gzReader, zerr := gzip.NewReader(resp.Body):

    • resp.Body は、受信したHTTPレスポンスのボディを表す io.ReadCloser です。
    • gzip.NewReader は、この resp.Body をラップし、gzip圧縮されたデータを透過的に解凍する新しい io.Reader (gzReader) を作成します。
    • この関数は、リーダーの作成に成功した場合は nil を、失敗した場合は errorzerr に返します。
  2. if zerr != nil { ... }:

    • 修正前は if err != nil でした。これは、gzip.NewReader の呼び出しとは無関係な、より外側のスコープで宣言された err 変数を参照していました。そのため、gzip.NewReader がエラーを返しても、この条件は真にならず、エラーが無視される可能性がありました。
    • 修正後は if zerr != nil となり、gzip.NewReader が返した特定のエラー変数 zerr を直接チェックするようになりました。これにより、gzipリーダーの初期化に失敗した場合に、適切なエラー処理パスに入ることが保証されます。
    • エラーが発生した場合、pc.close() が呼び出されて現在の持続接続が閉じられ、err = zerr によって、この readLoop 関数全体の戻り値として伝播されるべきエラー変数 errzerr の値が代入されます。

この修正により、net/http クライアントがgzip圧縮された不正なレスポンスボディを受信した場合でも、より堅牢なエラーハンドリングが可能になり、アプリケーションの安定性が向上しました。

関連リンク

参考にした情報源リンク

  • 提供されたコミット情報 (./commit_data/10786.txt)
  • Go言語の公式ドキュメントおよびパッケージドキュメント (一般的なGoのエラーハンドリング、net/http, compress/gzip の理解のため)
  • Go言語における変数スコープとシャドーイングに関する一般的な知識
  • GitHub上のコミットページ: https://github.com/golang/go/commit/c0421d92c87c9428a5f3f8e5457fda88db585c90