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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける Response.Write メソッドが、HTTPレスポンスのステータス行にステータスコードを重複して書き込む可能性があったバグを修正するものです。具体的には、Response.Status フィールドにステータスコードが既に含まれている場合に、Response.StatusCode から生成されるステータスコードと重複して表示される問題に対処しています。また、関連するテストケースの追加と、transfer.go における Request.Method の参照方法の改善も含まれています。

コミット

commit d45f22e3c843c4c19fd547684e51f249d9fd53dd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon May 21 11:07:27 2012 -0700

    net/http: fix duplicate status code in Response.Write
    
    Fixes #3636
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6203094

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

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

元コミット内容

net/http: Response.Write における重複ステータスコードの修正

Response.Write メソッドがHTTPレスポンスのステータス行にステータスコードを重複して書き込む問題を修正します。

関連するIssue: #3636

レビュー担当者: golang-dev, adg CC: golang-dev Go Change List (CL): https://golang.org/cl/6203094

変更の背景

このコミットは、Go言語の net/http パッケージにおける Response.Write メソッドのバグを修正するために行われました。具体的には、HTTPレスポンスのステータス行(例: HTTP/1.1 200 OK)を生成する際に、ステータスコードが重複して表示されるという問題がありました。

Goの net/http パッケージでは、http.Response 構造体がHTTPレスポンスを表します。この構造体には StatusCode (数値のステータスコード、例: 200) と Status (ステータスコードとテキストを含む文字列、例: "200 OK") の両方のフィールドが存在します。

問題は、Response.Write メソッドがステータス行を構築する際に、Response.Status フィールドの内容をそのまま使用しつつ、さらに Response.StatusCode から数値のステータスコードを文字列に変換して追加していた点にありました。もし Response.Status が既に "200 OK" のようにステータスコードを含んでいる場合、結果として "HTTP/1.1 200 200 OK" のようにステータスコードが重複して出力されてしまう可能性がありました。

このバグは、GoのIssueトラッカーで #3636 として報告されていました。このコミットは、その報告された問題を解決することを目的としています。

前提知識の解説

HTTPレスポンスのステータス行

HTTPレスポンスは、クライアントに返される情報であり、その最初の行は「ステータス行」と呼ばれます。ステータス行は以下の形式で構成されます。

HTTP-Version Status-Code Reason-Phrase

  • HTTP-Version: 使用されているHTTPプロトコルのバージョン(例: HTTP/1.1)。
  • Status-Code: 3桁の整数で、リクエストの結果を示します(例: 200 はOK、404 はNot Found)。
  • Reason-Phrase: ステータスコードを説明する短いテキスト(例: OKNot Found)。

例: HTTP/1.1 200 OK

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントとサーバーを実装するための基本的な機能を提供します。

  • http.Response 構造体: HTTPレスポンスを表す構造体です。
    • StatusCode int: レスポンスの数値ステータスコード(例: 200, 404)。
    • Status string: レスポンスのステータス行のテキスト部分(例: "200 OK", "404 Not Found")。通常、StatusCodeReason-Phrase を組み合わせたものです。
    • ProtoMajor int, ProtoMinor int: HTTPプロトコルのメジャーバージョンとマイナーバージョン(例: HTTP/1.1 の場合、ProtoMajor は1、ProtoMinor は1)。
  • Response.Write(w io.Writer) error メソッド: http.Response 構造体の内容を io.Writer (通常はネットワーク接続) に書き込み、完全なHTTPレスポンスを形成します。このメソッドが、ステータス行、ヘッダー、ボディなどを適切にフォーマットして出力します。
  • io.WriteString(w io.Writer, s string) (n int, err error): 指定された io.Writer に文字列 s を書き込むヘルパー関数です。

ステータスコードと理由句の生成ロジック

net/http パッケージでは、Response.Status フィールドが空の場合、Response.StatusCode に基づいてデフォルトの理由句(Reason-Phrase)を生成するロジックがあります。例えば、StatusCode が200であれば "OK" が、404であれば "Not Found" が自動的に補完されます。しかし、Response.Status が明示的に設定されている場合は、その値が優先されます。

このコミットの修正は、この Response.Status の扱いと、Response.StatusCode から生成される文字列との間の潜在的な重複を解消することに焦点を当てています。

技術的詳細

このコミットの主要な目的は、http.Response.Write メソッドがHTTPステータス行を生成する際に、ステータスコードが重複して出力される問題を解決することです。

問題点: 従来の Response.Write の実装では、ステータス行を構築する際に、まず Response.StatusCode から数値のステータスコードを文字列に変換し、その後に Response.Status フィールドの内容(これは既にステータスコードと理由句を含んでいる可能性がある)を結合していました。

例えば、Response.StatusCode200 で、Response.Status"200 OK" の場合、ステータス行は HTTP/1.1 200 200 OK のように、200 が重複して出力される可能性がありました。これはHTTPプロトコルに準拠しておらず、クライアント側で予期せぬ動作を引き起こす可能性があります。

解決策: このコミットでは、以下のロジックを導入することでこの問題を解決しています。

  1. Response.StatusCode を文字列に変換し、その後にスペースを追加した statusCode 変数(例: "200 ")を作成します。
  2. Response.Status フィールドから取得した text 変数(理由句)が、この statusCode で始まっているかどうかを strings.HasPrefix を使ってチェックします。
  3. もし textstatusCode で始まっている場合(つまり、Response.Status が既にステータスコードを含んでいる場合)、text からその重複するステータスコード部分を削除します。これにより、text は純粋な理由句(例: "OK")になります。
  4. 最終的に、HTTP/ProtoMajor.ProtoMinor statusCode text\r\n の形式でステータス行を構築します。この際、statusCode は数値のステータスコードとスペースを含み、text は重複が除去された理由句のみを含むため、ステータスコードの重複が回避されます。

transfer.go の変更: src/pkg/net/http/transfer.go の変更は、newTransferWriter 関数内で ResponseToHEAD フィールドを設定する際に、rr.Request.Method を直接参照するのではなく、t.Method を参照するように変更されています。これは、rr.Requestnil の場合にパニックを避けるための安全策であり、より堅牢なコードにするための改善です。t.Method は、rr.Request が存在する場合に既に設定されているため、この変更はロジックの堅牢性を高めます。

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

src/pkg/net/http/response.go

--- a/src/pkg/net/http/response.go
+++ b/src/pkg/net/http/response.go
@@ -202,9 +202,12 @@ func (r *Response) Write(w io.Writer) error {
 			text = "status code " + strconv.Itoa(r.StatusCode)
 		}
 	}\n-\tio.WriteString(w, "HTTP/"+strconv.Itoa(r.ProtoMajor)+".")
-\tio.WriteString(w, strconv.Itoa(r.ProtoMinor)+" ")
-\tio.WriteString(w, strconv.Itoa(r.StatusCode)+" "+text+"\\r\\n")
+\tprotoMajor, protoMinor := strconv.Itoa(r.ProtoMajor), strconv.Itoa(r.ProtoMinor)
+\tstatusCode := strconv.Itoa(r.StatusCode) + " "
+\tif strings.HasPrefix(text, statusCode) {
+\t\ttext = text[len(statusCode):]
+\t}
+\tio.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n")
 
 	// Process Body,ContentLength,Close,Trailer
 	tw, err := newTransferWriter(r)

src/pkg/net/http/response_test.go

--- a/src/pkg/net/http/response_test.go
+++ b/src/pkg/net/http/response_test.go
@@ -14,6 +14,7 @@ import (
 	"io/ioutil"
 	"net/url"
 	"reflect"
+	"strings"
 	"testing"
 )
 
@@ -444,3 +445,17 @@ func TestLocationResponse(t *testing.T) {
 		}
 	}
 }\n+\n+func TestResponseStatusStutter(t *testing.T) {\n+\tr := &Response{\n+\t\tStatus:     "123 some status",\n+\t\tStatusCode: 123,\n+\t\tProtoMajor: 1,\n+\t\tProtoMinor: 3,\n+\t}\n+\tvar buf bytes.Buffer\n+\tr.Write(&buf)\n+\tif strings.Contains(buf.String(), "123 123") {\n+\t\tt.Errorf("stutter in status: %s", buf.String())\n+\t}\n+}\n```

### `src/pkg/net/http/transfer.go`

```diff
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -71,7 +71,9 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
 			}
 		}
 	case *Response:\n-\t\tt.Method = rr.Request.Method
+\t\tif rr.Request != nil {\n+\t\t\tt.Method = rr.Request.Method\n+\t\t}\n \t\tt.Body = rr.Body
 \t\tt.BodyCloser = rr.Body
 \t\tt.ContentLength = rr.ContentLength
@@ -79,7 +81,7 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
 \t\tt.TransferEncoding = rr.TransferEncoding
 \t\tt.Trailer = rr.Trailer
 \t\tatLeastHTTP11 = rr.ProtoAtLeast(1, 1)\n-\t\tt.ResponseToHEAD = noBodyExpected(rr.Request.Method)\n+\t\tt.ResponseToHEAD = noBodyExpected(t.Method)\
 \t}\
 
 \t// Sanitize Body,ContentLength,TransferEncoding

コアとなるコードの解説

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

このファイルでは、Response.Write メソッド内のHTTPステータス行の生成ロジックが変更されています。

変更前:

	io.WriteString(w, "HTTP/"+strconv.Itoa(r.ProtoMajor)+".")
	io.WriteString(w, strconv.Itoa(r.ProtoMinor)+" ")
	io.WriteString(w, strconv.Itoa(r.StatusCode)+" "+text+"\\r\\n")

このコードでは、r.StatusCode を文字列に変換して出力し、その後に text (これは r.Status から取得される) を出力していました。もし text が既にステータスコードを含んでいる場合、ここで重複が発生していました。

変更後:

	protoMajor, protoMinor := strconv.Itoa(r.ProtoMajor), strconv.Itoa(r.ProtoMinor)
	statusCode := strconv.Itoa(r.StatusCode) + " "
	if strings.HasPrefix(text, statusCode) {
		text = text[len(statusCode):]
	}
	io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\\r\\n")
  1. protoMajorprotoMinor を文字列に変換して変数に格納します。
  2. r.StatusCode を文字列に変換し、末尾にスペースを追加した statusCode 変数を作成します(例: "200 ")。
  3. strings.HasPrefix(text, statusCode) を使用して、textstatusCode で始まるかどうかをチェックします。これは、r.Status が既にステータスコードを含んでいるかどうかを確認するためです。
  4. もし textstatusCode で始まる場合、text = text[len(statusCode):] によって、text の先頭から重複するステータスコード部分を削除します。これにより、text は純粋な理由句のみになります。
  5. 最後に、io.WriteString を一度だけ呼び出し、HTTP/、プロトコルバージョン、statusCode(数値ステータスコードとスペース)、そして修正された text(理由句)を結合して完全なステータス行を書き込みます。この方法により、ステータスコードの重複が確実に回避されます。

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

このファイルには、TestResponseStatusStutter という新しいテスト関数が追加されています。

func TestResponseStatusStutter(t *testing.T) {
	r := &Response{
		Status:     "123 some status",
		StatusCode: 123,
		ProtoMajor: 1,
		ProtoMinor: 3,
	}
	var buf bytes.Buffer
	r.Write(&buf)
	if strings.Contains(buf.String(), "123 123") {
		t.Errorf("stutter in status: %s", buf.String())
	}
}

このテストは、意図的に Response.Status にステータスコード("123")と理由句("some status")の両方を含ませ、StatusCode も同じ 123 に設定しています。 r.Write(&buf) を呼び出してレスポンスを bytes.Buffer に書き込み、その結果の文字列 buf.String()"123 123" という重複したステータスコードを含んでいないことを strings.Contains で確認しています。もし重複が見つかった場合、テストは失敗し、バグが修正されていないことを示します。このテストの追加により、将来的に同様の回帰バグが発生するのを防ぐことができます。

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

このファイルでは、newTransferWriter 関数内の ResponseToHEAD フィールドの設定方法が変更されています。

変更前:

	t.Method = rr.Request.Method
	// ...
	t.ResponseToHEAD = noBodyExpected(rr.Request.Method)

ResponseToHEAD の設定で rr.Request.Method を直接参照していました。

変更後:

	if rr.Request != nil {
		t.Method = rr.Request.Method
	}
	// ...
	t.ResponseToHEAD = noBodyExpected(t.Method)
  1. t.Method = rr.Request.Method の行に if rr.Request != nil のチェックが追加されました。これにより、rr.Requestnil の場合に発生する可能性のあるパニックを回避します。
  2. t.ResponseToHEAD = noBodyExpected(rr.Request.Method)t.ResponseToHEAD = noBodyExpected(t.Method) に変更されました。これは、t.Method が既に rr.Request.Method の値(もし rr.Requestnil でなければ)を持っているため、より安全で一貫性のある方法でメソッドを参照するようにしたものです。これにより、rr.Requestnil の場合でも、t.Method がデフォルト値(通常は空文字列)になり、noBodyExpected 関数が安全に呼び出されるようになります。

関連リンク

参考にした情報源リンク