[インデックス 10791] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージ内のtransport.go
ファイルに対する変更です。transport.go
は、HTTPクライアントがネットワーク接続を管理し、リクエストを送信し、レスポンスを受信する際の低レベルな詳細を扱う重要な部分です。具体的には、HTTPトランスポート層の実装が含まれており、コネクションの再利用(Keep-Alive)、プロキシの処理、TLSハンドシェイク、そしてレスポンスボディの読み取りとデコード(gzip圧縮など)といった機能を提供します。
コミット
このコミットは、HTTP HEAD
リクエストに対するレスポンスがgzip圧縮されているように見える場合に、net/http
パッケージのTransport
が誤ってgzip解凍を試み、その結果としてエラーが発生するバグを修正します。HEAD
リクエストはレスポンスボディを持たないため、ボディの解凍を試みるべきではありません。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/22dafc9bc5c8b339628a64c9f786491a60031005
元コミット内容
commit 22dafc9bc5c8b339628a64c9f786491a60031005
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Dec 14 11:20:21 2011 -0800
http: fix failing Transport HEAD request with gzip-looking response
We only want to attempt to un-gzip if there's a body (not in
response to a HEAD)
This was accidentally passing before, but revealed to be broken
when c3c6e72d7cc went in.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5477093
変更の背景
この変更の背景には、Goのnet/http
パッケージにおけるHTTP HEAD
リクエストの処理に関するバグが存在しました。
-
HEAD
リクエストの特性: HTTPHEAD
メソッドは、GET
メソッドと同様にリソースのヘッダー情報のみを取得するために使用されます。重要なのは、HEAD
リクエストに対するレスポンスにはメッセージボディが含まれないというHTTPの仕様です。サーバーはGET
リクエストに対するレスポンスと同じヘッダーを返しますが、ボディは送信しません。 -
バグの原因:
net/http
のTransport
実装では、レスポンスヘッダーにContent-Encoding: gzip
が含まれている場合、レスポンスボディをgzip解凍しようとするロジックがありました。しかし、HEAD
リクエストの場合、たとえヘッダーにContent-Encoding: gzip
が含まれていても、実際にはボディが存在しないため、gzip.NewReader
が非ボディデータ(例えば、ソケットのEOF)をgzipデータとして解釈しようとし、結果としてエラー("unexpected EOF"など)を発生させていました。 -
回帰(Regression): コミットメッセージによると、この問題は以前は「偶然にも通過していた」とされています。これは、おそらく以前のGoのバージョンや特定の条件下では、この誤った解凍試行が致命的なエラーにならなかったことを示唆しています。しかし、
c3c6e72d7cc
というコミットが導入されたことで、この潜在的なバグが顕在化し、HEAD
リクエストが失敗するようになりました。c3c6e72d7cc
は、GoのHTTPクライアントにおけるレスポンス処理の内部的な変更(例えば、コネクションの読み取りロジックやエラーハンドリングの厳密化)に関連している可能性が高いです。これにより、HEAD
リクエストに対する不適切なgzip解凍の試みが、以前は無視されていたか、異なる方法で処理されていたエラーとして認識されるようになったと考えられます。
このコミットは、HEAD
リクエストの特性を正しく考慮し、ボディが存在しない場合にはgzip解凍を試みないようにすることで、このバグを修正することを目的としています。
前提知識の解説
1. HTTP HEAD
メソッド
HTTP HEAD
メソッドは、Webサーバーからリソースのヘッダー情報のみを取得するために使用されます。これは、リソースの存在確認、最終更新日時、コンテンツタイプ、コンテンツサイズなどを、実際のコンテンツ(ボディ)をダウンロードせずに知りたい場合に非常に有用です。
- 目的:
GET
リクエストと同じヘッダーを取得するが、メッセージボディは含まない。 - 用途:
- リンクの有効性チェック。
- リソースのメタデータ(例:
Content-Type
,Content-Length
,Last-Modified
)の取得。 - キャッシュの検証(
If-Modified-Since
ヘッダーと組み合わせて)。
- 重要な特性: レスポンスにボディは含まれません。サーバーは
Content-Length
ヘッダーを送信する場合がありますが、これはGET
リクエストで返されるであろうボディのサイズを示すものであり、HEAD
レスポンス自体のボディサイズではありません。
2. HTTP Content-Encoding: gzip
Content-Encoding
ヘッダーは、メッセージボディに適用されたエンコーディング(通常は圧縮アルゴリズム)を示します。gzip
は最も一般的な圧縮方式の一つです。
- 目的: ネットワーク転送量を削減し、Webページのロード時間を短縮する。
- 仕組み: サーバーはレスポンスボディをgzipで圧縮して送信し、クライアント(ブラウザやHTTPクライアントライブラリ)はそれを受信して解凍します。
- 関連ヘッダー: クライアントは
Accept-Encoding: gzip
ヘッダーを送信して、gzip圧縮を受け入れ可能であることをサーバーに伝えます。
3. Go言語 net/http
パッケージの Transport
Goのnet/http
パッケージは、HTTPクライアントとサーバーを構築するための強力な機能を提供します。
http.Client
: HTTPリクエストを送信するための高レベルなインターフェースを提供します。http.Transport
:http.Client
の背後で動作し、実際のネットワーク通信(TCP接続の確立、TLSハンドシェイク、リクエストの書き込み、レスポンスの読み取りなど)を処理する低レベルな実装です。- コネクションプーリング:
Transport
は、HTTP/1.xのKeep-Aliveコネクションを再利用することで、新しいTCP接続を確立するオーバーヘッドを削減し、パフォーマンスを向上させます。 persistConn
:Transport
内部で、個々の永続的なTCPコネクションを管理する構造体です。このpersistConn
が、リクエストの送信とレスポンスの受信のループ(readLoop
)を担当します。readLoop
:persistConn
内で動作するゴルーチンで、ネットワークからレスポンスデータを継続的に読み取り、それを処理します。このループ内で、レスポンスヘッダーの解析やボディのデコード(gzipなど)が行われます。
- コネクションプーリング:
4. 回帰(Regression)
ソフトウェア開発における回帰とは、以前は正しく動作していた機能が、新しい変更(コミット)の導入によって動作しなくなる、またはバグが発生する現象を指します。このコミットの背景にある問題は、まさにこのような回帰によって顕在化しました。
技術的詳細
このコミットの技術的詳細は、Goのnet/http
パッケージにおけるHTTPレスポンスの処理フロー、特にTransport
のpersistConn
がどのようにレスポンスを読み取り、gzip解凍を試みるかに関連しています。
-
persistConn.readLoop()
:transport.go
内のpersistConn
構造体には、readLoop()
というメソッドがあります。これは、HTTPコネクションからレスポンスを非同期に読み取るためのゴルーチンとして実行されます。このループの主な役割は以下の通りです。- ネットワークから生データを読み取る。
http.ReadResponse
関数を使用して、生データからHTTPレスポンス(ヘッダーとボディ)をパースする。- パースされたレスポンスを、対応するリクエストを待っているクライアントに渡す。
-
gzip解凍のロジック:
readLoop
内でレスポンスが読み取られた後、GoクライアントがAccept-Encoding: gzip
ヘッダーをリクエストに追加していた場合(rc.addedGzip
がtrue)、かつレスポンスヘッダーにContent-Encoding: gzip
が含まれている場合、Transport
はレスポンスボディを自動的に解凍しようとします。これは、gzip.NewReader(resp.Body)
を呼び出すことで行われます。この関数は、提供されたresp.Body
(io.Reader
インターフェース)をラップし、読み取り時に自動的に解凍を行う新しいio.Reader
を返します。 -
HEAD
リクエストにおける問題: 前述の通り、HEAD
リクエストに対するレスポンスにはボディがありません。しかし、サーバーはGET
リクエストの場合に返されるであろうContent-Encoding: gzip
ヘッダーをHEAD
レスポンスにも含めることがあります。 この状況で、GoのTransport
は以下のように動作していました。rc.addedGzip
がtrue(クライアントがgzipを受け入れる設定)resp.Header.Get("Content-Encoding") == "gzip"
がtrue これらの条件が満たされると、gzip.NewReader(resp.Body)
が呼び出されます。しかし、resp.Body
はHEAD
リクエストのため空(またはEOF状態)です。gzip.NewReader
は、有効なgzipデータストリームを期待するため、空の入力や不完全な入力に対してはエラー(例:io.EOF
やgzip: invalid header
)を返します。このエラーが、HEAD
リクエストの失敗として報告されていました。
-
c3c6e72d7cc
による顕在化: コミットメッセージにあるc3c6e72d7cc
は、GoのHTTPクライアントの内部的な堅牢性やエラーハンドリングを改善したコミットであると推測されます。この変更により、以前は無視されていたか、異なる方法で処理されていたgzip.NewReader
からのエラーが、より厳密に扱われるようになり、結果としてHEAD
リクエストの失敗が顕在化したと考えられます。
このコミットは、gzip.NewReader
を呼び出す前に、レスポンスに実際にボディが存在するかどうかを明示的にチェックすることで、この問題を解決します。これにより、HEAD
リクエストのようにボディがない場合には、不必要なgzip解凍の試みを回避し、エラーを防ぎます。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -539,12 +539,13 @@ func (pc *persistConn) readLoop() {
resp, err := ReadResponse(pc.br, rc.req)
if err == nil {
- if rc.addedGzip && resp.Header.Get("Content-Encoding") == "gzip" {
+ hasBody := rc.req.Method != "HEAD" && resp.ContentLength != 0
+ if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" {
resp.Header.Del("Content-Encoding")
resp.Header.Del("Content-Length")
resp.ContentLength = -1
tgzReader, zerr := gzip.NewReader(resp.Body)
- if err != nil {
+ if zerr != nil {
pc.close()
err = zerr
} else {
コアとなるコードの解説
このコミットでは、src/pkg/net/http/transport.go
ファイルのpersistConn.readLoop()
関数内で、HTTPレスポンスのgzip解凍処理に関する2つの重要な変更が行われています。
-
hasBody
変数の導入と条件の追加: 変更前は、gzip解凍の条件はrc.addedGzip
(クライアントがgzipを受け入れる設定)とresp.Header.Get("Content-Encoding") == "gzip"
(レスポンスがgzipエンコードされていると宣言)の2つでした。 変更後、新たにhasBody
というブール変数が導入されました。hasBody := rc.req.Method != "HEAD" && resp.ContentLength != 0
この
hasBody
は、以下の2つの条件が両方とも真である場合にtrue
となります。rc.req.Method != "HEAD"
: 現在のリクエストメソッドがHEAD
ではないこと。HEAD
リクエストはボディを持たないため、この条件で除外されます。resp.ContentLength != 0
: レスポンスのContent-Length
ヘッダーが0ではないこと。Content-Length
が0の場合もボディがないことを示唆します。 そして、gzip解凍の条件式にこのhasBody
が追加されました。
if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" {
これにより、
HEAD
リクエストやContent-Length
が0のレスポンスに対しては、たとえContent-Encoding: gzip
ヘッダーが存在しても、不必要なgzip解凍の試みが回避されるようになりました。これは、HTTPの仕様に則った正しい振る舞いです。 -
エラー変数の修正 (
err
->zerr
): 変更前のコードでは、gzip.NewReader(resp.Body)
の呼び出し後に、その戻り値であるzerr
(gzipリーダー作成時のエラー)ではなく、外側のスコープのerr
変数をチェックしていました。// 変更前 tgzReader, zerr := gzip.NewReader(resp.Body) if err != nil { // ここでzerrではなくerrをチェックしていた pc.close() err = zerr } else { // ... }
これは論理的なバグであり、
gzip.NewReader
がエラーを返しても、そのエラーが適切に処理されない可能性がありました。 変更後、この部分がzerr
をチェックするように修正されました。// 変更後 tgzReader, zerr := gzip.NewReader(resp.Body) if zerr != nil { // zerrを正しくチェック pc.close() err = zerr } else { // ... }
この修正により、
gzip.NewReader
の呼び出しで発生したエラーが正しく捕捉され、コネクションのクローズやエラーの伝播が行われるようになりました。これは、コードの堅牢性を高めるための重要な修正です。
これらの変更により、GoのHTTPクライアントはHEAD
リクエストをより正確に処理し、不必要なエラーを回避できるようになりました。
関連リンク
- Go Code Review: https://golang.org/cl/5477093
参考にした情報源リンク
- HTTP/1.1 RFC 2616 - Method Definitions: https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html (特に9.4 HEAD)
- HTTP/1.1 RFC 2616 - Content-Encoding: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go
compress/gzip
package documentation: https://pkg.go.dev/compress/gzip - (
c3c6e72d7cc
に関する具体的な情報は見つかりませんでしたが、コミットメッセージからその影響を推測しました。)