[インデックス 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: ステータスコードを説明する短いテキスト(例:
OK
、Not 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")。通常、StatusCode
とReason-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.StatusCode
が 200
で、Response.Status
が "200 OK"
の場合、ステータス行は HTTP/1.1 200 200 OK
のように、200
が重複して出力される可能性がありました。これはHTTPプロトコルに準拠しておらず、クライアント側で予期せぬ動作を引き起こす可能性があります。
解決策: このコミットでは、以下のロジックを導入することでこの問題を解決しています。
Response.StatusCode
を文字列に変換し、その後にスペースを追加したstatusCode
変数(例:"200 "
)を作成します。Response.Status
フィールドから取得したtext
変数(理由句)が、このstatusCode
で始まっているかどうかをstrings.HasPrefix
を使ってチェックします。- もし
text
がstatusCode
で始まっている場合(つまり、Response.Status
が既にステータスコードを含んでいる場合)、text
からその重複するステータスコード部分を削除します。これにより、text
は純粋な理由句(例:"OK"
)になります。 - 最終的に、
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.Request
が nil
の場合にパニックを避けるための安全策であり、より堅牢なコードにするための改善です。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")
protoMajor
とprotoMinor
を文字列に変換して変数に格納します。r.StatusCode
を文字列に変換し、末尾にスペースを追加したstatusCode
変数を作成します(例: "200 ")。strings.HasPrefix(text, statusCode)
を使用して、text
がstatusCode
で始まるかどうかをチェックします。これは、r.Status
が既にステータスコードを含んでいるかどうかを確認するためです。- もし
text
がstatusCode
で始まる場合、text = text[len(statusCode):]
によって、text
の先頭から重複するステータスコード部分を削除します。これにより、text
は純粋な理由句のみになります。 - 最後に、
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)
t.Method = rr.Request.Method
の行にif rr.Request != nil
のチェックが追加されました。これにより、rr.Request
がnil
の場合に発生する可能性のあるパニックを回避します。t.ResponseToHEAD = noBodyExpected(rr.Request.Method)
がt.ResponseToHEAD = noBodyExpected(t.Method)
に変更されました。これは、t.Method
が既にrr.Request.Method
の値(もしrr.Request
がnil
でなければ)を持っているため、より安全で一貫性のある方法でメソッドを参照するようにしたものです。これにより、rr.Request
がnil
の場合でも、t.Method
がデフォルト値(通常は空文字列)になり、noBodyExpected
関数が安全に呼び出されるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/d45f22e3c843c4c19fd547684e51f249d9fd53dd
- Go Change List (CL): https://golang.org/cl/6203094
参考にした情報源リンク
- Go Issue 3636 (Go CL 6203094 にリンクされているため、直接のIssueページは特定できませんでしたが、CLページで詳細が確認できます。)
- HTTP/1.1: Header Field Definitions (RFC 2616): https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 (HTTPステータス行の形式について)
- Go
net/http
パッケージのドキュメント (当時のバージョン): https://pkg.go.dev/net/http (現在のドキュメントですが、当時のAPIも類似しています) - Go
strconv
パッケージのドキュメント: https://pkg.go.dev/strconv - Go
strings
パッケージのドキュメント: https://pkg.go.dev/strings - Go
bytes
パッケージのドキュメント: https://pkg.go.dev/bytes - Go
io
パッケージのドキュメント: https://pkg.go.dev/io