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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける ReadResponse 関数の挙動を改善するものです。具体的には、ReadResponse 関数が nilRequest パラメータを受け取った場合でも正しく動作するように修正されています。これにより、HTTPレスポンスの読み込み処理の柔軟性が向上しました。

コミット

  • コミットハッシュ: e3dbb1a31059994de2b20fedca0a8061cc90a7dd
  • Author: Taru Karttunen taruti@taruti.net
  • Date: Fri Aug 9 15:11:03 2013 -0700

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

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

元コミット内容

net/http: Make ReadResponse work with a nil Request parameter

Fixes #5583

R=golang-dev, dave, bradfitz
CC=golang-dev
https://golang.org/cl/9821043

変更の背景

net/http パッケージの ReadResponse 関数は、HTTPレスポンスを bufio.Reader から読み込むための関数です。この関数は、対応する http.Request オブジェクトを req パラメータとして受け取ります。以前の実装では、この req パラメータが nil であると、レスポンスボディの読み込み方法(特に Content-LengthTransfer-Encoding が指定されていない場合の挙動)を決定する際に問題が発生する可能性がありました。

HTTP/1.0 のレスポンスでは、Content-Length ヘッダがない場合、サーバーは接続を閉じることでレスポンスボディの終了を示します("Connection: close")。しかし、HTTP/1.1 では、Content-Length または Transfer-Encoding: chunked がない場合、レスポンスボディの終了はリクエストメソッドに依存することがあります。例えば、HEAD リクエストに対するレスポンスにはボディがないと見なされます。

このコミット以前は、ReadResponsenilRequest を受け取ると、レスポンスボディの長さを正しく判断できず、予期せぬ動作やエラーを引き起こす可能性がありました。この修正は、reqnil の場合でも、ReadResponse が堅牢に動作するようにするためのものです。具体的には、reqnil の場合は GET リクエストであると仮定することで、この問題を解決しています。

前提知識の解説

  • HTTP (Hypertext Transfer Protocol): Web上でデータを交換するためのプロトコル。リクエストとレスポンスの形式が定義されています。
  • HTTPリクエスト (http.Request): クライアントがサーバーに送信する情報。メソッド(GET, POSTなど)、URL、ヘッダ、ボディなどを含みます。
  • HTTPレスポンス (http.Response): サーバーがクライアントに返す情報。ステータスコード(200 OK, 404 Not Foundなど)、ヘッダ、ボディなどを含みます。
  • net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。
  • bufio.Reader: バッファリングされたI/O操作を提供するGoの型。効率的な読み込みを可能にします。
  • ReadResponse 関数: net/http パッケージ内の関数で、bufio.Reader からHTTPレスポンスを解析し、http.Response オブジェクトとして返します。
  • Content-Length ヘッダ: HTTPメッセージのボディのバイト数を示すヘッダ。
  • Transfer-Encoding: chunked ヘッダ: HTTP/1.1 で使用されるエンコーディング方式で、ボディをチャンク(塊)に分割して送信します。これにより、事前にボディの全長を知らなくてもストリーミングが可能になります。
  • Connection: close ヘッダ: HTTP/1.0 でよく使われるヘッダで、レスポンスの送信後に接続を閉じることを示します。これにより、ボディの終了を通知する役割も果たします。
  • RequestMethod: HTTPリクエストのメソッド(GET, POST, HEADなど)。レスポンスボディの処理に影響を与えることがあります。例えば、HEAD リクエストに対するレスポンスにはボディが含まれないことが期待されます。

技術的詳細

このコミットの主要な変更点は、net/http パッケージ内の ReadResponse 関数と、レスポンスボディの読み込みを処理する内部関数 readTransfer の挙動にあります。

  1. ReadResponse 関数の変更:

    • 以前の ReadResponse 関数のドキュメントでは、req パラメータは「このレスポンスに対応するリクエストを指定する」とされていました。
    • 修正後、ドキュメントは「req パラメータはオプションで、このレスポンスに対応するリクエストを指定する。nil の場合、GET リクエストであると仮定される」と変更されました。
    • コードレベルでは、resp = new(Response) ではなく resp := &Response{Request: req,} と初期化されるようになりました。これは、Response オブジェクトの Request フィールドが、関数呼び出し時に渡された req パラメータで直接初期化されることを意味します。これにより、resp.Request = req という明示的な代入が不要になります。
  2. readTransfer 関数の変更:

    • readTransfer 関数は、HTTPメッセージ(リクエストまたはレスポンス)のボディの読み込み方法を決定する内部関数です。この関数は、Content-LengthTransfer-Encoding ヘッダ、そしてリクエストメソッドに基づいてボディの長さを判断します。
    • 以前の実装では、transferReader 構造体の RequestMethod フィールドは、レスポンスの場合 rr.Request.Method から直接設定されていました。しかし、rr.Requestnil の場合、これはパニックを引き起こす可能性がありました。
    • 修正後、transferReader の初期化時に RequestMethod: "GET" がデフォルトで設定されるようになりました。
    • レスポンスを処理する case *Response: ブロック内で、rr.Requestnil でない場合にのみ t.RequestMethod = rr.Request.Method が実行されるようになりました。これにより、nil ポインタ参照によるパニックが回避されます。
    • リクエストを処理する case *Request: ブロックでは、以前は t.RequestMethod = "GET" が明示的に設定されていましたが、これは transferReader の初期化時に既に GET が設定されているため、削除されました。

これらの変更により、ReadResponse 関数が nilRequest パラメータを受け取った場合でも、readTransfer 関数が RequestMethod を安全に「GET」として扱い、レスポンスボディの読み込みロジックが正しく適用されるようになりました。特に、Content-LengthTransfer-Encoding がないHTTP/1.0のレスポンスにおいて、接続が閉じられるまでボディを読み込むという挙動が保証されます。

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

src/pkg/net/http/response.go

--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -98,18 +98,17 @@ func (r *Response) Location() (*url.URL, error) {
 	return url.Parse(lv)
 }
 
-// ReadResponse reads and returns an HTTP response from r.  The
-// req parameter specifies the Request that corresponds to
-// this Response.  Clients must call resp.Body.Close when finished
-// reading resp.Body.  After that call, clients can inspect
-// resp.Trailer to find key/value pairs included in the response
-// trailer.
-func ReadResponse(r *bufio.Reader, req *Request) (resp *Response, err error) {
-
+// ReadResponse reads and returns an HTTP response from r.
+// The req parameter optionally specifies the Request that corresponds
+// to this Response. If nil, a GET request is assumed.
+// Clients must call resp.Body.Close when finished reading resp.Body.
+// After that call, clients can inspect resp.Trailer to find key/value
+// pairs included in the response trailer.
+func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {
  	tp := textproto.NewReader(r)
-	resp = new(Response)
-
-	resp.Request = req
+	resp := &Response{
+		Request: req,
+	}
  
  	// Parse the first line of the response.
  	line, err := tp.ReadLine()

src/pkg/net/http/transfer.go

--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -254,7 +254,7 @@ func bodyAllowedForStatus(status int) bool {
 
 // msg is *Request or *Response.
 func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
-\tt := &transferReader{}\n+\tt := &transferReader{RequestMethod: "GET"}\n 
 \t// Unify input
 \tisResponse := false
 \tswitch rr := msg.(type) {
@@ -262,11 +262,13 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 \t\tt.Header = rr.Header
 \t\tt.StatusCode = rr.StatusCode
-\t\tt.RequestMethod = rr.Request.Method
 \t\tt.ProtoMajor = rr.ProtoMajor
 \t\tt.ProtoMinor = rr.ProtoMinor
 \t\tt.Close = shouldClose(t.ProtoMajor, t.ProtoMinor, t.Header)
 \t\tisResponse = true
+\t\tif rr.Request != nil {
+\t\t\tt.RequestMethod = rr.Request.Method
+\t\t}\n \tcase *Request:
 \t\tt.Header = rr.Header
 \t\tt.ProtoMajor = rr.ProtoMajor
 \t\tt.ProtoMinor = rr.ProtoMinor
@@ -274,7 +276,6 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 \t\t// Transfer semantics for Requests are exactly like those for
 \t\t// Responses with status code 200, responding to a GET method
 \t\tt.StatusCode = 200
-\t\tt.RequestMethod = "GET"\n \tdefault:
 \t\tpanic("unexpected type")
 \t}

src/pkg/net/http/response_test.go

テストケースが追加されています。Content-Length がなく Connection: close ヘッダを持つHTTP/1.0のレスポンスを、Requestnil の状態で ReadResponse が正しく処理できることを検証するテストです。

コアとなるコードの解説

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

  • ReadResponse 関数のシグネチャとコメントの変更:
    • 以前の func ReadResponse(r *bufio.Reader, req *Request) (resp *Response, err error) から func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) に変更されました。これは、名前付き戻り値 resp を削除し、より一般的なGoの慣習に従ったものです。
    • コメントが更新され、req パラメータがオプションであり、nil の場合は GET リクエストが仮定されることが明記されました。これは、関数の新しい挙動を明確に示しています。
  • Response オブジェクトの初期化:
    • resp = new(Response)resp.Request = req の2行が、resp := &Response{Request: req,} の1行に集約されました。これは、Response 構造体の新しいインスタンスを作成し、同時にその Request フィールドを渡された req パラメータで初期化する、より簡潔な方法です。機能的には同じですが、コードの可読性と簡潔性が向上しています。

src/pkg/net/http/transfer.go の変更

  • transferReader の初期化における RequestMethod のデフォルト設定:
    • t := &transferReader{}t := &transferReader{RequestMethod: "GET"} に変更されました。これは、transferReader のインスタンスが作成される際に、デフォルトで RequestMethod"GET" に設定されることを意味します。これにより、後続のロジックで RequestMethod が明示的に設定されない場合でも、安全なデフォルト値が保証されます。
  • レスポンス処理における RequestMethod の条件付き設定:
    • case *Response: ブロック内で、以前は無条件に t.RequestMethod = rr.Request.Method が実行されていました。これが if rr.Request != nil { t.RequestMethod = rr.Request.Method } に変更されました。この変更により、rr.Requestnil の場合に nil ポインタのデリファレンスによるパニックが発生するのを防ぎます。rr.Requestnil であれば、transferReader の初期化時に設定されたデフォルトの "GET" がそのまま使用されます。
  • リクエスト処理における RequestMethod の設定の削除:
    • case *Request: ブロック内で、以前は t.RequestMethod = "GET" が明示的に設定されていました。これは、transferReader の初期化時に既に "GET" がデフォルトで設定されているため、冗長となり削除されました。

これらの変更は、ReadResponse 関数が nilRequest パラメータを受け取った場合でも、HTTPレスポンスのボディの読み込みロジックが堅牢かつ安全に動作するようにするためのものです。特に、Content-LengthTransfer-Encoding ヘッダがない場合のボディの終了判断において、GET リクエストのセマンティクスをデフォルトで適用することで、予期せぬ動作を防ぎます。

関連リンク

参考にした情報源リンク