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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内のHTTPレスポンスの書き出しに関する修正とテストの追加を行っています。具体的には、以下のファイルが変更されています。

  • src/pkg/net/http/response.go: http.Response 構造体の Write メソッドの実装が修正されています。このメソッドは、http.Response オブジェクトを io.Writer に書き出す役割を担います。
  • src/pkg/net/http/response_test.go: http.Request のダミー生成関数 dummyReq11 が追加され、HTTP/1.1 リクエストのテストケースを容易に記述できるようになっています。
  • src/pkg/net/http/responsewrite_test.go: http.Response.Write メソッドの様々なエッジケースをカバーするための、多数の新しいテストケースが追加されています。

コミット

commit a30eaa12eb2ecb484d3ced8775fadeeb20a21569
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Apr 10 17:12:31 2014 -0700

    net/http: fix up Response.Write edge cases
    
    The Go HTTP server doesn't use Response.Write, but others do,
    so make it correct. Add a bunch more tests.
    
    This bug is almost a year old. :/
    
    Fixes #5381
    
    LGTM=adg
    R=golang-codereviews, adg
    CC=dsymonds, golang-codereviews, rsc
    https://golang.org/cl/85740046

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

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

元コミット内容

このコミットは、Go言語の net/http パッケージにおける Response.Write メソッドのエッジケースを修正することを目的としています。コミットメッセージによると、GoのHTTPサーバー自体はこの Response.Write メソッドを直接使用していませんが、他のコンポーネントやユーザーがこのメソッドを使用する可能性があるため、その正確性を確保する必要がありました。このバグは約1年前から存在していたと述べられており、#5381 というIssueを修正するものです。修正には、多数の追加テストが含まれています。

変更の背景

net/http パッケージの Response.Write メソッドは、http.Response オブジェクトを io.Writer に書き出すための汎用的な機能を提供します。GoのHTTPサーバーは通常、http.ResponseWriter インターフェースを介してクライアントに応答を書き出すため、Response.Write を直接使用することはありません。しかし、HTTPプロキシやカスタムHTTPクライアントなど、http.Response オブジェクトを構築し、それを任意の io.Writer に書き出す必要があるシナリオでは、このメソッドが利用されます。

このコミットが行われる前は、Response.Write メソッドにいくつかのエッジケースにおけるバグが存在していました。特に、ContentLength が不明 (-1) な場合や、ボディが空であるかどうかの判断、そしてHTTP/1.1における Connection: close ヘッダの適切な設定に関する問題があったと考えられます。これらの問題は、HTTPプロトコルの仕様に厳密に従わないレスポンスが生成される可能性があり、相互運用性の問題を引き起こす可能性がありました。コミットメッセージにある「This bug is almost a year old. :/」という記述から、この問題が長期間未解決であったことが伺えます。

この修正は、Response.Write メソッドが、GoのHTTPサーバーが使用しない場合でも、HTTPプロトコルの仕様に準拠した正しいレスポンスを生成できるようにするために行われました。これにより、このメソッドを利用する他のアプリケーションやライブラリの信頼性が向上します。

前提知識の解説

このコミットの変更内容を理解するためには、以下のHTTPプロトコルおよびGo言語の基本的な概念を理解しておく必要があります。

HTTP/1.0 と HTTP/1.1

  • HTTP/1.0: 各リクエスト/レスポンスのペアごとに新しいTCP接続を確立し、レスポンスの送信が完了すると接続を閉じることが一般的でした。Content-Length ヘッダは必須であり、レスポンスボディの長さを明示的に示す必要がありました。
  • HTTP/1.1: 持続的接続(Persistent Connections)が導入され、単一のTCP接続上で複数のリクエスト/レスポンスをやり取りできるようになりました。これにより、接続の確立・切断のオーバーヘッドが削減され、パフォーマンスが向上しました。HTTP/1.1では、Content-Length ヘッダがない場合でも、Transfer-Encoding: chunked を使用してボディを送信できます。

Content-Length ヘッダ

Content-Length ヘッダは、HTTPメッセージボディのオクテット(バイト)単位の長さを指定します。これは、受信側がメッセージボディの終わりを正確に判断するために重要です。

  • Content-Length: 0: ボディが空であることを明示的に示します。
  • Content-Length: -1 (Goの内部表現): Goの net/http パッケージでは、ContentLength フィールドが -1 の場合、レスポンスボディの長さが不明であることを示します。この場合、HTTP/1.1では Transfer-Encoding: chunked を使用するか、Connection: close ヘッダを付けて接続を閉じることでボディの終わりを示します。

Transfer-Encoding: chunked ヘッダ

Transfer-Encoding: chunked は、メッセージボディをチャンク(塊)に分割して送信するエンコーディング方式です。各チャンクは、そのチャンクのサイズを示す16進数と、それに続くデータで構成されます。ボディの終わりは、サイズが0のチャンクで示されます。この方式は、メッセージボディのサイズが事前に分からない場合や、動的に生成されるコンテンツをストリーミングする場合に特に有用です。Transfer-Encoding: chunked が使用される場合、Content-Length ヘッダは通常は送信されません。

Connection: close ヘッダ

Connection ヘッダは、現在のトランザクションが完了した後にネットワーク接続をどのように扱うかを制御します。

  • Connection: close: 送信側(クライアントまたはサーバー)が、現在のトランザクションの完了後に接続を閉じたいことを示します。HTTP/1.0ではこれがデフォルトの動作でした。
  • Connection: keep-alive: HTTP/1.1のデフォルトであり、接続を維持して後続のリクエスト/レスポンスに再利用したいことを示します。

Content-Length が不明なHTTP/1.1レスポンスで Transfer-Encoding: chunked が使用されない場合、受信側は接続が閉じられることでボディの終わりを判断します。このため、Connection: close ヘッダを明示的に設定する必要があります。

io.Readerio.Closer (Go言語)

  • io.Reader: データを読み出すためのインターフェースで、Read([]byte) (n int, err error) メソッドを持ちます。
  • io.Closer: リソースを閉じるためのインターフェースで、Close() error メソッドを持ちます。
  • http.Response.Bodyio.ReadCloser 型であり、これは io.Readerio.Closer の両方のインターフェースを満たします。レスポンスボディの読み出しと、そのリソースのクリーンアップ(接続の解放など)のために使用されます。

技術的詳細

このコミットの主要な技術的詳細は、http.Response.Write メソッドが、HTTPレスポンスのボディの長さが不明な場合 (ContentLength: -1) や、ボディが空である場合の挙動を正確に処理するように修正された点にあります。

修正前の Response.Write は、これらのエッジケースにおいてHTTPプロトコルの仕様に準拠しないレスポンスを生成する可能性がありました。特に問題となっていたのは以下の点です。

  1. ContentLength: 0 の扱い: Response.Bodynil または空の io.Reader で、かつ ContentLength0 に設定されている場合、Content-Length: 0 ヘッダが適切に送信されない可能性がありました。
  2. ContentLength: -1 (不明な長さ) の扱い: HTTP/1.1において、ContentLength-1 で、かつ Transfer-Encoding: chunked が使用されていない場合、レスポンスボディの終わりを示すために Connection: close ヘッダを送信する必要があります。修正前は、このケースで Connection: close が適切に設定されないことがありました。
  3. Body の先読みと ContentLength の調整: Response.Body が設定されているが ContentLength0 または -1 の場合、Response.Write はボディから少量のデータを先読みして、実際にボディが存在するかどうか、またはその長さが 0 であるかどうかを判断しようとします。この先読みのロジックが不完全であったため、ボディが実際には空でないにもかかわらず ContentLength: 0 と判断されたり、逆に空のボディに対して不適切な処理が行われたりする可能性がありました。

このコミットでは、これらの問題を解決するために以下の変更が加えられました。

  • Response オブジェクトのクローン: Response.Write メソッドの冒頭で、元の http.Response オブジェクトのコピー (r1) を作成し、このコピーに対して変更を加えるようにしました。これにより、元の Response オブジェクトが不意に変更されることを防ぎます。
  • ContentLength: 0 の厳密な処理: r1.ContentLength == 0 && r1.Body != nil のケースで、実際にボディが空であるかを r1.Body.Read(buf[:]) で1バイト読み出すことで確認します。
    • もし Read0 バイトを返し io.EOF であれば、ボディは本当に空であると判断し、r1.BodyeofReader (常に io.EOF を返すリーダー) に設定し直します。これにより、後続の処理で空のボディが正しく扱われるようになります。
    • もし Read1 バイトを返した場合(つまりボディが空ではない場合)、r1.ContentLength-1 に設定し、r1.Bodyio.MultiReader を使って先読みした1バイトと元のボディを結合したものに再構築します。これにより、ボディが空でないことが正しく伝わり、後続の処理で ContentLength が不明なボディとして扱われるようになります。
  • ContentLength: -1Connection: close の自動設定: r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) の条件が満たされる場合(つまり、HTTP/1.1で長さ不明、かつ Connection: close が明示的に設定されておらず、チャンクエンコーディングも使用されていない場合)、r1.Close = true を設定して Connection: close ヘッダが送信されるようにします。これは、HTTP/1.1の仕様において、ボディの終わりを接続のクローズで示すための重要な修正です。
  • Content-Length: 0 ヘッダの明示的な書き出し: r1.ContentLength == 0 && !chunked(r1.TransferEncoding) の場合、Content-Length: 0 ヘッダを明示的に書き出すようにしました。これにより、ボディが空であることを受信側に確実に伝えます。
  • テストの拡充: responsewrite_test.go に多数の新しいテストケースが追加され、上記の様々なエッジケース(長さ不明のボディ、空のボディ、Connection: close の挙動など)が網羅的に検証されるようになりました。これにより、将来的な回帰バグを防ぎ、Response.Write の堅牢性を高めています。

これらの変更により、http.Response.Write メソッドは、より多くのシナリオでHTTPプロトコルの仕様に準拠した正しいレスポンスを生成できるようになり、このメソッドを利用するアプリケーションの信頼性が向上しました。

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

src/pkg/net/http/response.gofunc (r *Response) Write(w io.Writer) error メソッド内の変更がコアとなります。

--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -8,6 +8,7 @@ package http
 
  import (
  	"bufio"
+	"bytes"
  	"crypto/tls"
  	"errors"
  	"io"
@@ -199,7 +200,6 @@ func (r *Response) ProtoAtLeast(major, minor int) bool {
  //
  // Body is closed after it is sent.
  func (r *Response) Write(w io.Writer) error {
--
  	// Status line
  	text := r.Status
  	if text == "" {
@@ -212,10 +212,45 @@ func (r *Response) ProtoAtLeast(major, minor int) bool {
  	protoMajor, protoMinor := strconv.Itoa(r.ProtoMajor), strconv.Itoa(r.ProtoMinor)
  	statusCode := strconv.Itoa(r.StatusCode) + " "
  	text = strings.TrimPrefix(text, statusCode)
-	io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n")
+	if _, err := io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n"); err != nil {
+		return err
+	}
+
+	// Clone it, so we can modify r1 as needed.
+	r1 := new(Response)
+	*r1 = *r
+	if r1.ContentLength == 0 && r1.Body != nil {
+		// Is it actually 0 length? Or just unknown?
+		var buf [1]byte
+		n, err := r1.Body.Read(buf[:])
+		if err != nil && err != io.EOF {
+			return err
+		}
+		if n == 0 {
+			// Reset it to a known zero reader, in case underlying one
+			// is unhappy being read repeatedly.
+			r1.Body = eofReader
+		} else {
+			r1.ContentLength = -1
+			r1.Body = struct {
+				io.Reader
+				io.Closer
+			}{
+				io.MultiReader(bytes.NewReader(buf[:1]), r.Body),
+				r.Body,
+			}
+		}
+	}
+	// If we're sending a non-chunked HTTP/1.1 response without a
+	// content-length, the only way to do that is the old HTTP/1.0
+	// way, by noting the EOF with a connection close, so we need
+	// to set Close.
+	if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) {
+		r1.Close = true
+	}
  
  	// Process Body,ContentLength,Close,Trailer
-	tw, err := newTransferWriter(r)
+	tw, err := newTransferWriter(r1)
  	if err != nil {
  		return err
  	}
@@ -230,8 +265,16 @@ func (r *Response) Write(w io.Writer) error {
  		return err
  	}
  
+	if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) {
+		if _, err := io.WriteString(w, "Content-Length: 0\\r\\n"); err != nil {
+			return err
+		}
+	}
+
  	// End-of-header
-	io.WriteString(w, "\\r\\n")
+	if _, err := io.WriteString(w, "\\r\\n"); err != nil {
+		return err
+	}
  
  	// Write body and trailer
  	err = tw.WriteBody(w)

コアとなるコードの解説

src/pkg/net/http/response.go の変更点

  1. bytes パッケージのインポート追加:

    +	"bytes"
    

    bytes.NewReader を使用するために bytes パッケージがインポートされました。これは、io.MultiReader を使ってボディを再構築する際に必要となります。

  2. ステータスライン書き込みのエラーハンドリング追加:

    -	io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n")
    +	if _, err := io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n"); err != nil {
    +		return err
    +	}
    

    ステータスラインの書き込み処理にエラーチェックが追加されました。これにより、書き込み中にエラーが発生した場合に早期に処理を終了し、エラーを返すようになります。

  3. Response オブジェクトのクローンとボディの長さの調整ロジック:

    	// Clone it, so we can modify r1 as needed.
    	r1 := new(Response)
    	*r1 = *r
    	if r1.ContentLength == 0 && r1.Body != nil {
    		// Is it actually 0 length? Or just unknown?
    		var buf [1]byte
    		n, err := r1.Body.Read(buf[:])
    		if err != nil && err != io.EOF {
    			return err
    		}
    		if n == 0 {
    			// Reset it to a known zero reader, in case underlying one
    			// is unhappy being read repeatedly.
    			r1.Body = eofReader
    		} else {
    			r1.ContentLength = -1
    			r1.Body = struct {
    				io.Reader
    				io.Closer
    			}{
    				io.MultiReader(bytes.NewReader(buf[:1]), r.Body),
    				r.Body,
    			}
    		}
    	}
    
    • r1 := new(Response); *r1 = *r で、元の Response オブジェクト r のシャローコピー r1 を作成します。これにより、Write メソッド内で r1 を変更しても、呼び出し元の r に影響を与えません。
    • if r1.ContentLength == 0 && r1.Body != nil の条件は、ContentLength0 と設定されているが、Bodynil ではない(つまり、ボディが存在する可能性がある)場合に実行されます。
    • r1.Body.Read(buf[:]) でボディから1バイトを先読みします。
      • n == 0 の場合(1バイトも読み込めず、io.EOF が返された場合)、ボディは本当に空であると判断し、r1.BodyeofReader (常に io.EOF を返す特別なリーダー) に設定し直します。これは、一部の io.Reader 実装が複数回読み込まれると問題を起こす可能性があるため、安全策として行われます。
      • n > 0 の場合(1バイト読み込めた場合)、ボディは空ではないと判断し、r1.ContentLength-1 (長さ不明) に設定します。そして、io.MultiReader を使用して、先読みした1バイトと元のボディを結合した新しい io.ReadCloserr1.Body に設定します。これにより、先読みしたバイトが失われることなく、後続の処理でボディ全体が正しく読み込まれるようになります。
  4. ContentLength: -1 の場合の Connection: close 自動設定ロジック:

    	// If we're sending a non-chunked HTTP/1.1 response without a
    	// content-length, the only way to do that is the old HTTP/1.0
    	// way, by noting the EOF with a connection close, so we need
    	// to set Close.
    	if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) {
    		r1.Close = true
    	}
    

    この重要なブロックは、HTTP/1.1レスポンスにおいて、ボディの長さが不明 (ContentLength == -1) で、かつ Connection: close が明示的に設定されておらず (!r1.Close)、チャンクエンコーディングも使用されていない (!chunked(r1.TransferEncoding)) 場合に実行されます。この条件が満たされる場合、r1.Close = true を設定し、レスポンスヘッダに Connection: close が含まれるようにします。これにより、受信側は接続が閉じられることでボディの終わりを判断できるようになり、HTTP/1.1の仕様に準拠した正しい挙動となります。

  5. newTransferWriter の引数を r から r1 へ変更:

    -	tw, err := newTransferWriter(r)
    +	tw, err := newTransferWriter(r1)
    

    newTransferWriter 関数に渡す Response オブジェクトが、元の r から、修正が加えられたクローン r1 に変更されました。これにより、上記のロジックで調整された r1ContentLengthClose の値が、転送の書き出し処理に正しく反映されるようになります。

  6. Content-Length: 0 ヘッダの明示的な書き出し:

    	if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) {
    		if _, err := io.WriteString(w, "Content-Length: 0\\r\\n"); err != nil {
    			return err
    		}
    	}
    

    r1.ContentLength0 で、かつチャンクエンコーディングが使用されていない場合、Content-Length: 0 ヘッダを明示的に書き出すようにしました。これにより、ボディが空であることを受信側に確実に伝えます。

  7. ヘッダ終了を示す空行書き込みのエラーハンドリング追加:

    -	io.WriteString(w, "\\r\\n")
    +	if _, err := io.WriteString(w, "\\r\\n"); err != nil {
    +		return err
    +	}
    

    ヘッダの終わりを示す空行 (\r\n) の書き込み処理にもエラーチェックが追加されました。

src/pkg/net/http/response_test.go の変更点

  • dummyReq11 関数の追加:
    func dummyReq11(method string) *Request {
    	return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1}
    }
    
    HTTP/1.1プロトコルを使用するダミーの http.Request オブジェクトを生成するためのヘルパー関数が追加されました。これにより、HTTP/1.1に特化したテストケースを簡潔に記述できるようになります。

src/pkg/net/http/responsewrite_test.go の変更点

  • ioutil.NopCloser(bytes.NewBufferString("abcdef")) から ioutil.NopCloser(strings.NewReader("abcdef")) への変更:

    -				Body:          ioutil.NopCloser(bytes.NewBufferString("abcdef")),
    +				Body:          ioutil.NopCloser(strings.NewReader("abcdef")),
    

    テストケースで使用されるボディの生成方法が bytes.NewBufferString から strings.NewReader に変更されました。機能的には大きな違いはありませんが、strings.NewReader の方が文字列から io.Reader を生成する意図が明確です。

  • 多数の新しいテストケースの追加: このファイルには、Response.Write の様々なエッジケースを検証するための、非常に多くの新しいテストケースが追加されています。これには、以下のようなシナリオが含まれます。

    • ContentLength が不明 (-1) で、Connection: close が明示的に設定されている場合とそうでない場合のHTTP/1.1レスポンス。
    • ContentLength が不明 (-1) で、Transfer-Encoding: chunked が使用されているHTTP/1.1レスポンス。
    • ContentLength: 0 で、ボディが nil の場合、空の io.Reader の場合、非空の io.Reader の場合のHTTP/1.1レスポンス。 これらのテストケースは、Response.Write メソッドがHTTPプロトコルの複雑なルールに正確に準拠していることを保証するために不可欠です。

関連リンク

参考にした情報源リンク