[インデックス 16630] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける Transport
の挙動を修正するものです。具体的には、HTTPレスポンスのボディが Content-Length
ヘッダで示された長さよりも短い場合に、io.EOF
ではなく io.ErrUnexpectedEOF
を返すように変更しています。これにより、クライアント側で予期せぬボディの途切れを正確に検知できるようになります。
コミット
commit a054028471d08b5344d5cdb2781259fed84b6f7b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Jun 24 13:27:56 2013 -0700
net/http: Transport should return an error when response body ends early
If a server response contains a Content-Length and the body is short,
the Transport should end in io.ErrUnexpectedEOF, not io.EOF.
Fixes #5738
R=golang-dev, kevlar, r
CC=golang-dev
https://golang.org/cl/10237050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a054028471d08b5344d5cdb2781259fed84b6f7b
元コミット内容
net/http: Transport should return an error when response body ends early
If a server response contains a Content-Length and the body is short,
the Transport should end in io.ErrUnexpectedEOF, not io.EOF.
Fixes #5738
変更の背景
この変更の背景には、HTTP/1.1プロトコルにおける Content-Length
ヘッダの厳密な解釈と、Goの net/http
クライアントがその違反をどのように扱うべきかという問題があります。
HTTP/1.1では、Content-Length
ヘッダはメッセージボディのオクテット長を示します。クライアントは、このヘッダの値と実際に受信したボディの長さが一致することを期待します。もしサーバーが Content-Length
を指定しているにもかかわらず、それよりも短いボディを送信した場合、これはプロトコル違反であり、クライアントはこれをエラーとして扱うべきです。
しかし、このコミット以前の net/http.Transport
は、このような状況で io.EOF
(End Of File) を返していました。io.EOF
は通常、ストリームの正常な終端を示すために使用されます。そのため、ボディが途中で途切れた場合でも、クライアント側ではそれが正常な終端であるかのように扱われてしまい、予期せぬデータ欠損やアプリケーションの誤動作につながる可能性がありました。
この問題は Go issue #5738 として報告されており、このコミットはその修正を目的としています。io.ErrUnexpectedEOF
は、予期せぬファイルの終端、つまりデータが途中で途切れたことを示すためにGoの io
パッケージで定義されているエラーです。このエラーを返すことで、クライアントはサーバーからの不正なレスポンスを明確に識別し、適切にエラーハンドリングできるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の標準ライブラリの概念とHTTPプロトコルの知識が必要です。
-
io.EOF
:io
パッケージで定義されているエラーで、入力ストリームが正常に終端に達したことを示します。例えば、ファイルの内容をすべて読み込んだ後や、ネットワーク接続が正常に閉じられた後にRead
メソッドが返すエラーです。これはエラーというよりは、読み取りが完了したことを示すシグナルとして扱われることが多いです。 -
io.ErrUnexpectedEOF
:io
パッケージで定義されているエラーで、予期せぬ入力ストリームの終端を示します。これは、期待されるデータ量に満たない状態でストリームが終了した場合に発生します。例えば、Content-Length
が指定されているにもかかわらず、その長さよりも短いデータしか受信できなかった場合などに使用されます。これは明確なエラー状態を示します。 -
HTTP
Content-Length
ヘッダ: HTTPレスポンスヘッダの一つで、メッセージボディのオクテット長(バイト数)を示します。このヘッダが存在する場合、クライアントは指定されたバイト数のボディを受信することを期待します。 -
net/http.Transport
:net/http
パッケージにおけるHTTPクライアントの低レベルな実装を提供する構造体です。HTTPリクエストの送信、レスポンスの受信、接続の管理(キープアライブなど)を担当します。通常、http.Client
の内部で使用されます。 -
io.LimitedReader
:io
パッケージで提供されるReader
の実装の一つです。指定されたバイト数(N
フィールド)までしか読み取らないように元のReader
を制限します。Read
メソッドが呼び出され、読み取られたバイト数がN
に達すると、それ以降の読み取りではio.EOF
を返します。これは、Content-Length
が指定されたHTTPボディを読み取る際に、指定された長さ以上のデータを読み取らないようにするためにnet/http
パッケージの内部でよく使用されます。 -
bufio.Reader
:bufio
パッケージで提供されるバッファリングされたReader
です。I/O操作の効率を向上させるために、内部にバッファを持ち、一度に多くのデータを読み取ってから、要求に応じて少しずつ提供します。
技術的詳細
このコミットが修正する問題は、net/http
パッケージがHTTPレスポンスボディを読み取る際の io.EOF
の扱いに関するものです。
HTTP/1.1では、レスポンスボディの長さを示す方法として主に以下の2つがあります。
Content-Length
ヘッダ: ボディの正確なバイト数を事前に示す。Transfer-Encoding: chunked
: ボディをチャンク(塊)に分割して送信し、最後のチャンクで終端を示す。
net/http.Transport
は、これらのメカニズムに基づいてレスポンスボディを読み取ります。Content-Length
が指定されている場合、Transport
は内部的に io.LimitedReader
を使用して、指定されたバイト数までしかボディを読み取らないようにします。io.LimitedReader
は、その N
フィールドがゼロになった(つまり、指定されたバイト数をすべて読み取った)時点で io.EOF
を返します。
問題は、サーバーが Content-Length
で X
バイトを宣言したにもかかわらず、実際に X
バイト未満のボディを送信し、接続を閉じてしまった場合に発生していました。この場合、io.LimitedReader
は X
バイトに達する前に、基となるネットワーク接続からの io.EOF
を受け取ります。この io.EOF
は、io.LimitedReader
の N
がまだゼロになっていない(つまり、期待されるバイト数をすべて読み取っていない)にもかかわらず発生するため、これは「予期せぬ終端」と見なされるべきです。
しかし、コミット前のコードでは、body.Read
メソッド(net/http/transfer.go
内)が io.EOF
を受け取った際に、それがチャンクエンコーディングの終端であるか、または Content-Length
が指定されたボディの予期せぬ終端であるかを適切に区別していませんでした。結果として、Content-Length
が指定されたボディが途中で途切れた場合でも、io.EOF
がそのまま返されてしまい、クライアント側では正常な読み取り完了と誤解される可能性がありました。
このコミットは、body.Read
メソッド内で io.EOF
を受け取った際に、現在の Reader
が io.LimitedReader
であり、かつまだ読み取るべきバイト数 (lr.N
) が残っているかどうかをチェックするロジックを追加することで、この問題を解決しています。もしこれらの条件が満たされる場合、それは予期せぬ終端であると判断し、io.ErrUnexpectedEOF
を返すように修正されました。
コアとなるコードの変更箇所
このコミットによる変更は主に以下の2つのファイルにあります。
-
src/pkg/net/http/response_test.go
:TestResponseContentLengthShortBody
という新しいテストケースが追加されました。このテストは、Content-Length
ヘッダが指定されているにもかかわらず、実際のボディがその長さよりも短い場合に、io.ErrUnexpectedEOF
が返されることを検証します。
-
src/pkg/net/http/transfer.go
:body
型のRead
メソッドが修正されました。このメソッドは、HTTPレスポンスボディを読み取るための内部的なio.Reader
の実装です。- 既存の
if err == io.EOF
ブロック内に、Content-Length
が指定されたケースを特別に処理するロジックが追加されました。
コアとなるコードの解説
src/pkg/net/http/response_test.go
の変更
func TestResponseContentLengthShortBody(t *testing.T) {
const shortBody = "Short body, not 123 bytes."
br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" +
"Content-Length: 123\r\n" +
"\r\n" +
shortBody))
res, err := ReadResponse(br, &Request{Method: "GET"})
if err != nil {
t.Fatal(err)
}
if res.ContentLength != 123 {
t.Fatalf("Content-Length = %d; want 123", res.ContentLength)
}
var buf bytes.Buffer
n, err := io.Copy(&buf, res.Body)
if n != int64(len(shortBody)) {
t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody)
}
if buf.String() != shortBody {
t.Errorf("Read body %q; want %q", buf.String(), shortBody)
}
if err != io.ErrUnexpectedEOF {
t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)
}
}
このテストケースは、Content-Length: 123
と宣言されているにもかかわらず、実際のボディが shortBody
(長さ26バイト) であるHTTPレスポンスをシミュレートします。
ReadResponse
でレスポンスを読み込み、io.Copy
を使ってボディの内容を buf
にコピーします。
重要なのは最後の if err != io.ErrUnexpectedEOF
のアサーションです。これにより、ボディが途中で途切れた場合に io.ErrUnexpectedEOF
が返されることを期待しています。このテストがパスすることで、修正が正しく機能していることが確認できます。
src/pkg/net/http/transfer.go
の変更
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -532,13 +532,22 @@ func (b *body) Read(p []byte) (n int, err error) {
}
n, err = b.Reader.Read(p)
- // Read the final trailer once we hit EOF.
- if err == io.EOF && b.hdr != nil {
- if e := b.readTrailer(); e != nil {
- err = e
+ if err == io.EOF {
+ // Chunked case. Read the trailer.
+ if b.hdr != nil { // b.hdr が nil でない場合、チャンクエンコーディングのトレーラーを処理
+ if e := b.readTrailer(); e != nil {
+ err = e
+ }
+ b.hdr = nil
+ } else {
+ // If the server declared the Content-Length, our body is a LimitedReader
+ // and we need to check whether this EOF arrived early.
+ // サーバーが Content-Length を宣言した場合、ボディは LimitedReader であり、
+ // この EOF が早期に到着したかどうかを確認する必要がある。
+ if lr, ok := b.Reader.(*io.LimitedReader); ok && lr.N > 0 {
+ err = io.ErrUnexpectedEOF // 期待されるバイト数が残っているのに EOF が来た場合、ErrUnexpectedEOF を返す
+ }
}
- b.hdr = nil
}
-
return n, err
}
この変更は、body
型の Read
メソッド内で行われています。
元のコードでは、b.Reader.Read(p)
が io.EOF
を返した場合、それがチャンクエンコーディングの終端であるかどうかのチェック(b.hdr != nil
)のみを行っていました。
修正後のコードでは、if err == io.EOF
ブロックが拡張されています。
- チャンクエンコーディングのケース:
b.hdr != nil
の場合、これはチャンクエンコーディングのトレーラーを読み取る必要があることを示しており、以前と同様にreadTrailer()
を呼び出します。 Content-Length
のケース:b.hdr == nil
の場合(つまりチャンクエンコーディングではない場合)、さらに以下のチェックが行われます。if lr, ok := b.Reader.(*io.LimitedReader); ok && lr.N > 0
:b.Reader.(*io.LimitedReader)
: 現在のb.Reader
がio.LimitedReader
型に型アサーションできるかを確認します。Content-Length
が指定されたHTTPレスポンスボディの場合、net/http
は内部的にio.LimitedReader
を使用します。ok
: 型アサーションが成功したかどうか。lr.N > 0
:io.LimitedReader
のN
フィールドは、まだ読み取るべき残りのバイト数を示します。もしN
が0より大きいのにio.EOF
が返された場合、それは期待されるバイト数をすべて読み取る前にストリームが終了したことを意味します。
これらの条件がすべて真である場合、つまり Content-Length
が指定されたボディで、まだ読み取るべきデータが残っているにもかかわらず io.EOF
が発生した場合は、err = io.ErrUnexpectedEOF
とエラーを上書きします。これにより、クライアントは予期せぬボディの途切れを正確に検知できるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/a054028471d08b5344d5cdb2781259fed84b6f7b
- Go Issue #5738: https://golang.org/issue/5738
- Go CL 10237050: https://golang.org/cl/10237050
参考にした情報源リンク
- Go言語の公式ドキュメント (
io
パッケージ,net/http
パッケージ) - HTTP/1.1 RFC (特に
Content-Length
ヘッダに関するセクション) - Go言語のソースコード (
src/pkg/net/http/transfer.go
,src/pkg/net/http/response_test.go
)