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

[インデックス 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プロトコルの知識が必要です。

  1. io.EOF: io パッケージで定義されているエラーで、入力ストリームが正常に終端に達したことを示します。例えば、ファイルの内容をすべて読み込んだ後や、ネットワーク接続が正常に閉じられた後に Read メソッドが返すエラーです。これはエラーというよりは、読み取りが完了したことを示すシグナルとして扱われることが多いです。

  2. io.ErrUnexpectedEOF: io パッケージで定義されているエラーで、予期せぬ入力ストリームの終端を示します。これは、期待されるデータ量に満たない状態でストリームが終了した場合に発生します。例えば、Content-Length が指定されているにもかかわらず、その長さよりも短いデータしか受信できなかった場合などに使用されます。これは明確なエラー状態を示します。

  3. HTTP Content-Length ヘッダ: HTTPレスポンスヘッダの一つで、メッセージボディのオクテット長(バイト数)を示します。このヘッダが存在する場合、クライアントは指定されたバイト数のボディを受信することを期待します。

  4. net/http.Transport: net/http パッケージにおけるHTTPクライアントの低レベルな実装を提供する構造体です。HTTPリクエストの送信、レスポンスの受信、接続の管理(キープアライブなど)を担当します。通常、http.Client の内部で使用されます。

  5. io.LimitedReader: io パッケージで提供される Reader の実装の一つです。指定されたバイト数(N フィールド)までしか読み取らないように元の Reader を制限します。Read メソッドが呼び出され、読み取られたバイト数が N に達すると、それ以降の読み取りでは io.EOF を返します。これは、Content-Length が指定されたHTTPボディを読み取る際に、指定された長さ以上のデータを読み取らないようにするために net/http パッケージの内部でよく使用されます。

  6. 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-LengthX バイトを宣言したにもかかわらず、実際に X バイト未満のボディを送信し、接続を閉じてしまった場合に発生していました。この場合、io.LimitedReaderX バイトに達する前に、基となるネットワーク接続からの io.EOF を受け取ります。この io.EOF は、io.LimitedReaderN がまだゼロになっていない(つまり、期待されるバイト数をすべて読み取っていない)にもかかわらず発生するため、これは「予期せぬ終端」と見なされるべきです。

しかし、コミット前のコードでは、body.Read メソッド(net/http/transfer.go 内)が io.EOF を受け取った際に、それがチャンクエンコーディングの終端であるか、または Content-Length が指定されたボディの予期せぬ終端であるかを適切に区別していませんでした。結果として、Content-Length が指定されたボディが途中で途切れた場合でも、io.EOF がそのまま返されてしまい、クライアント側では正常な読み取り完了と誤解される可能性がありました。

このコミットは、body.Read メソッド内で io.EOF を受け取った際に、現在の Readerio.LimitedReader であり、かつまだ読み取るべきバイト数 (lr.N) が残っているかどうかをチェックするロジックを追加することで、この問題を解決しています。もしこれらの条件が満たされる場合、それは予期せぬ終端であると判断し、io.ErrUnexpectedEOF を返すように修正されました。

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

このコミットによる変更は主に以下の2つのファイルにあります。

  1. src/pkg/net/http/response_test.go:

    • TestResponseContentLengthShortBody という新しいテストケースが追加されました。このテストは、Content-Length ヘッダが指定されているにもかかわらず、実際のボディがその長さよりも短い場合に、io.ErrUnexpectedEOF が返されることを検証します。
  2. 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 ブロックが拡張されています。

  1. チャンクエンコーディングのケース: b.hdr != nil の場合、これはチャンクエンコーディングのトレーラーを読み取る必要があることを示しており、以前と同様に readTrailer() を呼び出します。
  2. Content-Length のケース: b.hdr == nil の場合(つまりチャンクエンコーディングではない場合)、さらに以下のチェックが行われます。
    • if lr, ok := b.Reader.(*io.LimitedReader); ok && lr.N > 0:
      • b.Reader.(*io.LimitedReader): 現在の b.Readerio.LimitedReader 型に型アサーションできるかを確認します。Content-Length が指定されたHTTPレスポンスボディの場合、net/http は内部的に io.LimitedReader を使用します。
      • ok: 型アサーションが成功したかどうか。
      • lr.N > 0: io.LimitedReaderN フィールドは、まだ読み取るべき残りのバイト数を示します。もし N が0より大きいのに io.EOF が返された場合、それは期待されるバイト数をすべて読み取る前にストリームが終了したことを意味します。

これらの条件がすべて真である場合、つまり Content-Length が指定されたボディで、まだ読み取るべきデータが残っているにもかかわらず io.EOF が発生した場合は、err = io.ErrUnexpectedEOF とエラーを上書きします。これにより、クライアントは予期せぬボディの途切れを正確に検知できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (io パッケージ, net/http パッケージ)
  • HTTP/1.1 RFC (特に Content-Length ヘッダに関するセクション)
  • Go言語のソースコード (src/pkg/net/http/transfer.go, src/pkg/net/http/response_test.go)