[インデックス 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.LimitReader
io/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の変更履歴とレビュープロセスについて)