[インデックス 17850] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http/httputil パッケージにおける DumpRequestOut 関数のバグ修正に関するものです。具体的には、HTTPリクエストの Content-Length ヘッダが設定されているにもかかわらず、リクエストボディをダンプしない設定(body パラメータが false)で DumpRequestOut を呼び出した際に発生する問題を解決します。
コミット
commit 1428045469302d81a6bc19ae9f1dd1e2905ea855
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Oct 29 14:06:11 2013 -0700
net/http/httputil: fix DumpRequestOut with ContentLength & false body param
Fixes #6471
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/14920050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1428045469302d81a6bc19ae9f1dd1e2905ea855
元コミット内容
net/http/httputil: fix DumpRequestOut with ContentLength & false body param
このコミットは、DumpRequestOut 関数が Content-Length ヘッダを持つリクエストを処理する際に、ボディをダンプしない設定(body パラメータが false)であった場合に発生する問題を修正します。
変更の背景
net/http/httputil パッケージは、HTTPリクエストやレスポンスをデバッグ目的でダンプ(文字列化)するためのユーティリティを提供します。DumpRequestOut 関数は、http.Transport が追加するヘッダ(例: User-Agent)を含む、クライアントから送信される形式のリクエストをダンプするために使用されます。
このコミット以前の DumpRequestOut の実装には、特定のシナリオで問題がありました。HTTPリクエストが Content-Length ヘッダ(ボディの長さを指定する)を持っているにもかかわらず、DumpRequestOut の body 引数が false に設定されている場合、つまりリクエストボディの内容自体はダンプしたくない場合に問題が発生しました。
Goの net/http パッケージ内部では、リクエストをワイヤー形式に変換する際に、Content-Length が設定されている場合はリクエストボディ (req.Body) からそのバイト数だけを読み取ろうとします。しかし、DumpRequestOut で body が false に設定されていると、req.Body は nil に設定されていました。nil の io.Reader から読み取ろうとすると、予期せぬエラーやパニックが発生する可能性がありました。
このバグは、Issue #6471 で報告されたものと関連していると考えられます。ユーザーが Content-Length を持つリクエストをダンプしようとした際に、期待通りの出力が得られない、またはエラーが発生するといった挙動が観測された可能性があります。
前提知識の解説
net/httpパッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。net/http/httputilパッケージ:net/httpパッケージのユーティリティ機能を提供し、特にHTTPリクエスト/レスポンスのダンプやリバースプロキシの実装に利用されます。http.Request構造体: HTTPリクエストを表すGoの構造体です。Body: リクエストボディを表すio.ReadCloserインターフェース。ContentLength: リクエストボディの長さをバイト単位で示すint64型のフィールド。
io.Readerインターフェース: データを読み取るための基本的なインターフェースで、Read([]byte) (n int, err error)メソッドを持ちます。ioutil.NopCloser:io.Readerをio.ReadCloserに変換するユーティリティ関数。Closeメソッドは何もしません。io.LimitReader: 指定されたバイト数までしか読み取らないio.Readerを作成する関数。- HTTP
Content-Lengthヘッダ: HTTPメッセージのエンティティボディのサイズをオクテット単位で示すヘッダです。このヘッダが存在する場合、受信側は指定されたバイト数だけボディを読み取ることを期待します。 - HTTPリクエストのワイヤー形式: HTTPリクエストがネットワーク上を流れる際のバイト列の形式。ヘッダとボディが特定のフォーマットで連結されます。
技術的詳細
このコミットの主要な変更点は、DumpRequestOut 関数が req.ContentLength がゼロでなく、かつ body パラメータが false の場合に、ダミーのボディを一時的に設定するようになったことです。
以前の実装では、body が false の場合、req.Body は単純に nil に設定されていました。しかし、Content-Length が設定されている場合、net/http の内部処理(特にリクエストをワイヤー形式に変換する部分)は、req.Body から Content-Length で指定されたバイト数を読み取ろうとします。req.Body が nil だと、この読み取り操作が失敗します。
新しい実装では、この問題を回避するために以下の手順を踏みます。
bodyがfalseで、かつreq.ContentLengthがゼロでない場合、ダミーのio.Readerを作成します。- このダミーのリーダーは、
neverEndingという新しい型で実装されており、常に指定されたバイト(この場合は'x')を返すように設計されています。 io.LimitReaderを使用して、このneverEndingリーダーをreq.ContentLengthで指定されたバイト数に制限します。ioutil.NopCloserを使用して、この制限されたリーダーをio.ReadCloserにラップし、一時的にreq.Bodyに設定します。これにより、内部のダンプ処理がContent-Lengthに従ってボディを「読み取る」ことが可能になります。dummyBodyというフラグをtrueに設定し、ダミーボディが使用されたことを記録します。- ダンプ処理が完了した後、
dumpされたバイト列からダミーボディの内容を削除します。これは、HTTPヘッダとボディの区切りである\r\n\r\nを探し、それ以降のバイトを切り捨てることで行われます。これにより、出力されるダンプにはヘッダのみが含まれ、ダミーボディの内容は含まれません。
このアプローチにより、DumpRequestOut は Content-Length ヘッダを正しく含んだリクエストヘッダを生成しつつ、実際にボディの内容をダンプすることなく、内部的なボディ読み取りの要件を満たすことができます。
テストコード (dump_test.go) では、Content-Length が 6 でボディが "abcdef" であるPOSTリクエストを定義し、DumpRequestOut を NoBody: true (つまり body=false) で呼び出す新しいテストケースが追加されています。このテストは、出力されるダンプが Content-Length: 6 ヘッダを含みつつ、ボディの内容を含まないことを検証します。
コアとなるコードの変更箇所
src/pkg/net/http/httputil/dump.go
type neverEnding byte
func (b neverEnding) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = byte(b)
}
return len(p), nil
}
func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
save := req.Body
+ dummyBody := false
if !body || req.Body == nil {
req.Body = nil
+ if req.ContentLength != 0 {
+ req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength))
+ dummyBody = true
+ }
} else {
var err error
save, req.Body, err = drainBody(req.Body)
@@ -99,7 +113,19 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
if err != nil {
return nil, err
}
- return buf.Bytes(), nil
+ dump := buf.Bytes()
+
+ // If we used a dummy body above, remove it now.
+ // TODO: if the req.ContentLength is large, we allocate memory
+ // unnecessarily just to slice it off here. But this is just
+ // a debug function, so this is acceptable for now. We could
+ // discard the body earlier if this matters.
+ if dummyBody {
+ if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 {
+ dump = dump[:i+4]
+ }
+ }
+ return dump, nil
}
src/pkg/net/http/httputil/dump_test.go
var dumpTests = []dumpTest{
@@ -83,6 +84,31 @@ var dumpTests = []dumpTest{
"User-Agent: Go 1.1 package http\r\n" +\
"Accept-Encoding: gzip\r\n\r\n",
},\
+
+ // Request with Body, but Dump requested without it.
+ {
+ Req: http.Request{
+ Method: "POST",
+ URL: &url.URL{
+ Scheme: "http",
+ Host: "post.tld",
+ Path: "/",
+ },
+ ContentLength: 6,
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ },
+
+ Body: []byte("abcdef"),
+
+ WantDumpOut: "POST / HTTP/1.1\r\n" +\
+ "Host: post.tld\r\n" +\
+ "User-Agent: Go 1.1 package http\r\n" +\
+ "Content-Length: 6\r\n" +\
+ "Accept-Encoding: gzip\r\n\r\n",
+
+ NoBody: true,
+ },
}
func TestDumpRequest(t *testing.T) {
@@ -105,7 +131,7 @@ func TestDumpRequest(t *testing.T) {
if tt.WantDump != "" {
setBody()
- dump, err := DumpRequest(&tt.Req, true)
+ dump, err := DumpRequest(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequest #%d: %s", i, err)
continue
@@ -118,7 +144,7 @@ func TestDumpRequest(t *testing.T) {
if tt.WantDumpOut != "" {
setBody()
- dump, err := DumpRequestOut(&tt.Req, true)
+ dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequestOut #%d: %s", i, err)
continue
コアとなるコードの解説
neverEnding 型と Read メソッド
type neverEnding byte
func (b neverEnding) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = byte(b)
}
return len(p), nil
}
この新しい型 neverEnding は、io.Reader インターフェースを満たすように定義されています。その Read メソッドは、渡されたバイトスライス p を、neverEnding 型のインスタンスが持つバイト値(この場合は 'x')で埋め尽くします。これは、io.LimitReader と組み合わせて、指定された長さのダミーボディを生成するために使用されます。これにより、net/http の内部処理がボディを読み取ろうとした際に、実際にデータが提供されるように見せかけることができます。
DumpRequestOut 関数の変更
func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
save := req.Body
+ dummyBody := false
if !body || req.Body == nil {
req.Body = nil
+ if req.ContentLength != 0 {
+ req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength))
+ dummyBody = true
+ }
} else {
var err error
save, req.Body, err = drainBody(req.Body)
この部分が修正の核心です。
dummyBodyという新しいブール変数が導入され、ダミーボディが使用されたかどうかを追跡します。if !body || req.Body == nilの条件ブロック内で、req.ContentLength != 0という追加の条件がチェックされます。- もし
ContentLengthがゼロでなければ、neverEnding('x')を基にしたio.LimitReaderが作成され、req.ContentLengthで指定されたバイト数に制限されます。 - このリーダーは
ioutil.NopCloserでラップされ、一時的にreq.Bodyに割り当てられます。これにより、Content-Lengthが存在するにもかかわらずreq.Bodyがnilであることによる問題を回避します。 dummyBodyがtrueに設定されます。
if err != nil {
return nil, err
}
- return buf.Bytes(), nil
+ dump := buf.Bytes()
+
+ // If we used a dummy body above, remove it now.
+ // TODO: if the req.ContentLength is large, we allocate memory
+ // unnecessarily just to slice it off here. But this is just
+ // a debug function, so this is acceptable for now. We could
+ // discard the body earlier if this matters.
+ if dummyBody {
+ if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 {
+ dump = dump[:i+4]
+ }
+ }
+ return dump, nil
}
ダンプ処理が完了した後、dummyBody が true であれば、生成されたダンプからダミーボディの内容を削除します。
bytes.Index(dump, []byte("\r\n\r\n"))を使用して、HTTPヘッダとボディの区切りである空行 (\r\n\r\n) のインデックスを探します。- インデックスが見つかった場合、ダンプされたバイト列をそのインデックスの4バイト先(
\r\n\r\nの直後)で切り詰めます。これにより、ヘッダのみが残り、ダミーボディとして追加された'x'のシーケンスは出力から除外されます。 - コメントにあるように、この方法は大きな
Content-Lengthの場合に不要なメモリ割り当てが発生する可能性を指摘していますが、デバッグ関数であるため許容範囲とされています。
テストコードの変更
+ // Request with Body, but Dump requested without it.
+ {
+ Req: http.Request{
+ Method: "POST",
+ URL: &url.URL{
+ Scheme: "http",
+ Host: "post.tld",
+ Path: "/",
+ },
+ ContentLength: 6,
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ },
+
+ Body: []byte("abcdef"),
+
+ WantDumpOut: "POST / HTTP/1.1\r\n" +\
+ "Host: post.tld\r\n" +\
+ "User-Agent: Go 1.1 package http\r\n" +\
+ "Content-Length: 6\r\n" +\
+ "Accept-Encoding: gzip\r\n\r\n",
+
+ NoBody: true,
+ },
新しいテストケースが dumpTests スライスに追加されました。このテストケースは、ContentLength が 6 に設定されたPOSTリクエストをシミュレートし、NoBody: true を設定することで、DumpRequestOut がボディをダンプしないように指示します。WantDumpOut は、Content-Length: 6 ヘッダが含まれるが、ボディの内容は含まれないことを期待する出力文字列を定義しています。
if tt.WantDump != "" {
setBody()
- dump, err := DumpRequest(&tt.Req, true)
+ dump, err := DumpRequest(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequest #%d: %s", i, err)
continue
@@ -118,7 +144,7 @@ func TestDumpRequest(t *testing.T) {
if tt.WantDumpOut != "" {
setBody()
- dump, err := DumpRequestOut(&tt.Req, true)
+ dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequestOut #%d: %s", i, err)
continue
既存のテストループ内で DumpRequest と DumpRequestOut を呼び出す際に、body 引数に !tt.NoBody が渡されるように変更されました。これにより、新しく追加された NoBody フィールドを使って、各テストケースでボディをダンプするかどうかを制御できるようになります。
関連リンク
- Go Issue #6471: このコミットが修正したとされる問題のトラッキング。ただし、直接的な検索結果は見つかりませんでした。
- Go Gerrit Change 14920050: このコミットに対応するGerritの変更履歴。
参考にした情報源リンク
- Go言語の公式ドキュメント:
net/httpおよびnet/http/httputilパッケージ ioパッケージのドキュメント:io.Reader,io.LimitReaderio/ioutilパッケージのドキュメント:ioutil.NopCloser- https://pkg.go.dev/io/ioutil (Go 1.16以降は
ioおよびosパッケージに移行)
- https://pkg.go.dev/io/ioutil (Go 1.16以降は
- HTTP/1.1 RFC 2616 (Content-Length ヘッダに関する情報)
- Go言語のIssueトラッカー (一般的なGoのバグ報告と修正プロセスについて)
- Go言語のGerritコードレビューシステム (一般的なGoの変更履歴とレビュープロセスについて)