[インデックス 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