[インデックス 10242] ファイルの概要
コミット
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージから、HTTPリクエスト/レスポンスのダンプ機能とチャンクエンコーディング関連のユーティリティ関数をnet/http/httputil
パッケージに移動させるリファクタリングです。これにより、net/http
コアパッケージの軽量化と責務の分離が図られています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/28564d60ebce78a4e151d8f18e2d15a574fd43a4
元コミット内容
httputil: move dump and chunking functions out of http
This moves DumpRequest, DumpResponse, NewChunkedReader,
and NewChunkedWriter out of http, as part of the continued
http diet plan.
Also, adds DumpRequestOut (for dumping outbound requests),
since DumpRequest's ambiguity (the "wire representation" in
what direction?) was often a source of confusion and bug
reports.
R=rsc, adg
CC=golang-dev
https://golang.org/cl/5339041
変更の背景
この変更の主な背景には、Go言語のnet/http
パッケージの「ダイエット計画 (http diet plan)」があります。これは、コアとなるnet/http
パッケージのサイズと複雑さを削減し、より専門的な機能やデバッグ用途のユーティリティを別のサブパッケージ(この場合はnet/http/httputil
)に分離するという方針を指します。
具体的には、以下の点が挙げられます。
- 責務の分離:
DumpRequest
やDumpResponse
のようなデバッグ・診断目的の関数、およびNewChunkedReader
/NewChunkedWriter
のような低レベルのチャンクエンコーディング処理は、HTTPプロトコルの基本的な動作に必須ではありません。これらをhttputil
(HTTPユーティリティ)パッケージに移動することで、net/http
パッケージはHTTPプロトコルのコア機能に集中できるようになります。 - APIの明確化と混乱の解消:
DumpRequest
は、リクエストの「ワイヤー表現」(ネットワーク上を流れる形式)をダンプする機能を提供しますが、これが「受信したリクエスト」と「送信するリクエスト」のどちらを指すのかが不明瞭で、ユーザーからの混乱やバグレポートの原因となっていました。このコミットでは、特に「送信するリクエスト」を明確にダンプするためのDumpRequestOut
関数が追加され、APIの意図がより明確になりました。 - パッケージの保守性向上: コアパッケージが肥大化すると、その保守や変更が困難になります。関連性の低い機能を分離することで、各パッケージのコードベースが小さくなり、理解しやすさ、テストのしやすさ、そして将来的な変更の容易さが向上します。
前提知識の解説
Go言語のパッケージシステム
Go言語では、コードは「パッケージ」という単位で整理されます。パッケージは関連する機能の集合であり、他のパッケージからインポートして利用できます。標準ライブラリも多数のパッケージで構成されており、net/http
はHTTPクライアントとサーバーの実装を提供し、net/http/httputil
はHTTPプロトコルに関連する様々なユーティリティ機能を提供します。
HTTP/1.1のチャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1では、メッセージボディの長さを事前に知らなくてもデータを送信できる「チャンク転送エンコーディング」というメカニズムがあります。これは、特に動的に生成されるコンテンツや、大きなファイルをストリーミングする際に有用です。データは「チャンク」と呼ばれる小さなブロックに分割され、各チャンクは自身のサイズ情報と共に送信されます。最後のチャンクはサイズが0で、メッセージの終わりを示します。
Transfer-Encoding: chunked
: HTTPヘッダーでこの値が指定されている場合、メッセージボディはチャンク形式でエンコードされていることを示します。NewChunkedWriter
: 生のデータをチャンク形式に変換して書き込むためのライター。NewChunkedReader
: チャンク形式でエンコードされたデータを読み込み、元のデータにデコードするためのリーダー。
HTTPリクエスト/レスポンスのダンプ (Dump)
HTTPリクエストやレスポンスの「ダンプ」とは、それらがネットワーク上を流れる際の生のバイト列(ワイヤー表現)を再現することです。これは、デバッグやプロトコル解析の際に非常に役立ちます。例えば、クライアントが送信したリクエストがサーバーでどのように解釈されているか、あるいはサーバーからのレスポンスがクライアントでどのように受信されているかを確認するために使用されます。
DumpRequest
: HTTPリクエストのワイヤー表現をダンプする関数。DumpResponse
: HTTPレスポンスのワイヤー表現をダンプする関数。DumpRequestOut
: このコミットで追加された関数で、特にGoのhttp.Transport
がリクエストを送信する際に付加するヘッダー(例:User-Agent
)を含めて、送信されるリクエストのワイヤー表現をダンプします。
技術的詳細
このコミットは、主に以下の技術的な変更を含んでいます。
- ファイルの移動と削除:
src/pkg/net/http/dump.go
が削除され、その内容はsrc/pkg/net/http/httputil/dump.go
に移動されました。src/pkg/net/http/chunked.go
からチャンクエンコーディング関連の公開関数(NewChunkedWriter
,NewChunkedReader
)が削除され、src/pkg/net/http/httputil/chunked.go
に移動されました。元のchunked.go
に残った関数は、パッケージ内部でのみ使用されるように小文字で始まる非公開関数(newChunkedWriter
,newChunkedReader
)に変更されました。
httputil
パッケージへの機能追加:httputil/dump.go
には、既存のDumpRequest
、DumpResponse
に加えて、新たにDumpRequestOut
が追加されました。DumpRequestOut
は、http.Transport
が内部的にリクエストを処理するメカニズムを模倣し、User-Agent
などの自動的に追加されるヘッダーを含んだ形で送信リクエストをダンプできるように設計されています。これは、実際のネットワーク通信に近い形でリクエストを再現するために重要です。httputil/chunked.go
には、NewChunkedWriter
とNewChunkedReader
の公開バージョンが実装されました。特にNewChunkedReader
の実装は興味深く、io.MultiReader
とhttp.ReadRequest
を組み合わせて、チャンクエンコードされたストリームをHTTPリクエストボディとして「偽装」することで、既存のhttp
パッケージのチャンクデコードロジックを再利用しています。これは、コードの重複を避けるための巧妙なハックとコメントされています。
- テストファイルの移動と追加:
httputil/chunked_test.go
とhttputil/dump_test.go
が新規作成され、移動された機能のテストが追加されました。これにより、機能が正しく動作することを確認し、将来の変更に対する回帰を防ぎます。net/http/requestwrite_test.go
からは、DumpRequest
に関連するテストケースが削除されました。これは、DumpRequest
がnet/http
パッケージから移動されたためです。
- 依存関係の更新:
src/pkg/net/http/Makefile
とsrc/pkg/net/http/httputil/Makefile
が更新され、ファイルの移動と追加がビルドシステムに反映されました。src/pkg/net/http/request.go
からdumpWrite
メソッドとreqWriteExcludeHeaderDump
マップが削除されました。これは、DumpRequest
のロジックがhttputil
に移動したためです。src/pkg/net/http/transfer.go
では、チャンクエンコーディングの内部的な利用箇所で、NewChunkedWriter
とNewChunkedReader
の代わりに、非公開化されたnewChunkedWriter
とnewChunkedReader
が呼び出されるように変更されました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集約されます。
-
src/pkg/net/http/chunked.go
:NewChunkedWriter
とNewChunkedReader
が非公開のnewChunkedWriter
とnewChunkedReader
にリネームされ、外部から直接呼び出せなくなりました。
--- a/src/pkg/net/http/chunked.go +++ b/src/pkg/net/http/chunked.go @@ -7,23 +7,10 @@ package http import ( "bufio" "io" - "log" "strconv" ) -// NewChunkedWriter returns a new writer that translates writes into HTTP -// "chunked" format before writing them to w. Closing the returned writer -// sends the final 0-length chunk that marks the end of the stream. -// -// NewChunkedWriter is not needed by normal applications. The http -// package adds chunking automatically if handlers don't set a -// Content-Length header. Using NewChunkedWriter inside a handler -// would result in double chunking or chunking with a Content-Length -// length, both of which are wrong. -func NewChunkedWriter(w io.Writer) io.WriteCloser { - if _, bad := w.(*response); bad { - log.Printf("warning: using NewChunkedWriter in an http.Handler; expect corrupt output") - } +func newChunkedWriter(w io.Writer) io.WriteCloser { return &chunkedWriter{w} } @@ -65,12 +52,6 @@ func (cw *chunkedWriter) Close() error { return err } -// NewChunkedReader returns a new reader that translates the data read from r -// out of HTTP "chunked" format before returning it. -// The reader returns io.EOF when the final 0-length chunk is read. -// -// NewChunkedReader is not needed by normal applications. The http package -// automatically decodes chunking when reading response bodies. -func NewChunkedReader(r *bufio.Reader) io.Reader { +func newChunkedReader(r *bufio.Reader) io.Reader { return &chunkedReader{r: r} }
-
src/pkg/net/http/dump.go
:- ファイル全体が削除されました。
-
src/pkg/net/http/httputil/chunked.go
(新規ファイル):NewChunkedWriter
とNewChunkedReader
の公開バージョンが実装されました。特にNewChunkedReader
は、http.ReadRequest
を内部的に利用する巧妙な実装になっています。
// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package httputil import ( "bufio" "http" // Note: This refers to the http package, not httputil "io" "strconv" "strings" ) // NewChunkedWriter returns a new writer that translates writes into HTTP // "chunked" format before writing them to w. Closing the returned writer // sends the final 0-length chunk that marks the end of the stream. // // NewChunkedWriter is not needed by normal applications. The http // package adds chunking automatically if handlers don't set a // Content-Length header. Using NewChunkedWriter inside a handler // would result in double chunking or chunking with a Content-Length // length, both of which are wrong. func NewChunkedWriter(w io.Writer) io.WriteCloser { return &chunkedWriter{w} } // Writing to ChunkedWriter translates to writing in HTTP chunked Transfer // Encoding wire format to the underlying Wire writer. type chunkedWriter struct { Wire io.Writer } // Write the contents of data as one chunk to Wire. // NOTE: Note that the corresponding chunk-writing procedure in Conn.Write has // a bug since it does not check for success of io.WriteString func (cw *chunkedWriter) Write(data []byte) (n int, err error) { // Don't send 0-length data. It looks like EOF for chunked encoding. if len(data) == 0 { return 0, nil } head := strconv.Itob(len(data), 16) + "\r\n" if _, err = io.WriteString(cw.Wire, head); err != nil { return 0, err } if n, err = cw.Wire.Write(data); err != nil { return } if n != len(data) { err = io.ErrShortWrite return } _, err = io.WriteString(cw.Wire, "\r\n") return } func (cw *chunkedWriter) Close() error { _, err := io.WriteString(cw.Wire, "0\r\n") return err } // NewChunkedReader returns a new reader that translates the data read from r // out of HTTP "chunked" format before returning it. // The reader returns io.EOF when the final 0-length chunk is read. // // NewChunkedReader is not needed by normal applications. The http package // automatically decodes chunking when reading response bodies. func NewChunkedReader(r io.Reader) io.Reader { // This is a bit of a hack so we don't have to copy chunkedReader into // httputil. It's a bit more complex than chunkedWriter, which is copied // above. req, err := http.ReadRequest(bufio.NewReader(io.MultiReader( strings.NewReader("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"), r, strings.NewReader("\r\n")))) if err != nil { panic("bad fake request: " + err.Error()) } return req.Body }
-
src/pkg/net/http/httputil/dump.go
(新規ファイル):DumpRequest
、DumpResponse
、そして新しく追加されたDumpRequestOut
が実装されました。
// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package httputil import ( "bytes" "errors" "fmt" "http" "io" "io/ioutil" "net" "strings" ) // One of the copies, say from b to r2, could be avoided by using a more // elaborate trick where the other copy is made during Request/Response.Write. // This would complicate things too much, given that these functions are for // debugging only. func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { var buf bytes.Buffer if _, err = buf.ReadFrom(b); err != nil { return nil, nil, err } if err = b.Close(); err != nil { return nil, nil, err } return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewBuffer(buf.Bytes())), nil } // dumpConn is a net.Conn which writes to Writer and reads from Reader type dumpConn struct { io.Writer io.Reader } func (c *dumpConn) Close() error { return nil } func (c *dumpConn) LocalAddr() net.Addr { return nil } func (c *dumpConn) RemoteAddr() net.Addr { return nil } func (c *dumpConn) SetTimeout(nsec int64) error { return nil } func (c *dumpConn) SetReadTimeout(nsec int64) error { return nil } func (c *dumpConn) SetWriteTimeout(nsec int64) error { return nil } // DumpRequestOut is like DumpRequest but includes // headers that the standard http.Transport adds, // such as User-Agent. func DumpRequestOut(req *http.Request, body bool) (dump []byte, err error) { save := req.Body if !body || req.Body == nil { req.Body = nil } else { save, req.Body, err = drainBody(req.Body) if err != nil { return } } var b bytes.Buffer dialed := false t := &http.Transport{ Dial: func(net, addr string) (c net.Conn, err error) { if dialed { return nil, errors.New("unexpected second dial") } c = &dumpConn{ Writer: &b, Reader: strings.NewReader("HTTP/1.1 500 Fake Error\r\n\r\n"), } return }, } _, err = t.RoundTrip(req) req.Body = save if err != nil { return } dump = b.Bytes() return } // Return value if nonempty, def otherwise. func valueOrDefault(value, def string) string { if value != "" { return value } return def } var reqWriteExcludeHeaderDump = map[string]bool{ "Host": true, // not in Header map anyway "Content-Length": true, "Transfer-Encoding": true, "Trailer": true, } // dumpAsReceived writes req to w in the form as it was received, or // at least as accurately as possible from the information retained in // the request. func dumpAsReceived(req *http.Request, w io.Writer) error { return nil } // DumpRequest returns the as-received wire representation of req, // optionally including the request body, for debugging. // DumpRequest is semantically a no-op, but in order to // dump the body, it reads the body data into memory and // changes req.Body to refer to the in-memory copy. // The documentation for http.Request.Write details which fields // of req are used. func DumpRequest(req *http.Request, body bool) (dump []byte, err error) { save := req.Body if !body || req.Body == nil { req.Body = nil } else { save, req.Body, err = drainBody(req.Body) if err != nil { return } } var b bytes.Buffer urlStr := req.URL.Raw if urlStr == "" { urlStr = valueOrDefault(req.URL.EncodedPath(), "/") if req.URL.RawQuery != "" { urlStr += "?" + req.URL.RawQuery } } fmt.Fprintf(&b, "%s %s HTTP/%d.%d\r\n", valueOrDefault(req.Method, "GET"), urlStr, req.ProtoMajor, req.ProtoMinor) host := req.Host if host == "" && req.URL != nil { host = req.URL.Host } if host != "" { fmt.Fprintf(&b, "Host: %s\r\n", host) } chunked := len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" if len(req.TransferEncoding) > 0 { fmt.Fprintf(&b, "Transfer-Encoding: %s\r\n", strings.Join(req.TransferEncoding, ",")) } if req.Close { fmt.Fprintf(&b, "Connection: close\r\n") } err = req.Header.WriteSubset(&b, reqWriteExcludeHeaderDump) if err != nil { return } io.WriteString(&b, "\r\n") if req.Body != nil { var dest io.Writer = &b if chunked { dest = NewChunkedWriter(dest) } _, err = io.Copy(dest, req.Body) if chunked { dest.(io.Closer).Close() io.WriteString(&b, "\r\n") } } req.Body = save if err != nil { return } dump = b.Bytes() return } // DumpResponse is like DumpRequest but dumps a response. func DumpResponse(resp *http.Response, body bool) (dump []byte, err error) { var b bytes.Buffer save := resp.Body savecl := resp.ContentLength if !body || resp.Body == nil { resp.Body = nil resp.ContentLength = 0 } else { save, resp.Body, err = drainBody(resp.Body) if err != nil { return } } err = resp.Write(&b) resp.Body = save resp.ContentLength = savecl if err != nil { return } dump = b.Bytes() return }
-
src/pkg/net/http/transfer.go
:- 内部的なチャンクエンコーディングの処理で、
NewChunkedWriter
とNewChunkedReader
の代わりに、非公開化されたnewChunkedWriter
とnewChunkedReader
が使用されるように変更されました。
--- a/src/pkg/net/http/transfer.go +++ b/src/pkg/net/http/transfer.go @@ -187,7 +187,7 @@ func (t *transferWriter) WriteBody(w io.Writer) (err error) { // Write body if t.Body != nil { if chunked(t.TransferEncoding) { - cw := NewChunkedWriter(w) + cw := newChunkedWriter(w) _, err = io.Copy(cw, t.Body) if err == nil { err = cw.Close() @@ -319,7 +319,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) { // or close connection when finished, since multipart is not supported yet switch { case chunked(t.TransferEncoding): - t.Body = &body{Reader: NewChunkedReader(r), hdr: msg, r: r, closing: t.Close} + t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close} case t.ContentLength >= 0: // TODO: limit the Content-Length. This is an easy DoS vector. t.Body = &body{Reader: io.LimitReader(r, t.ContentLength), closing: t.Close}
- 内部的なチャンクエンコーディングの処理で、
コアとなるコードの解説
このコミットの核心は、net/http
パッケージの「ダイエット」と、デバッグ・ユーティリティ機能のnet/http/httputil
への移管です。
-
net/http/chunked.go
の変更:NewChunkedWriter
とNewChunkedReader
がnewChunkedWriter
とnewChunkedReader
にリネームされたことは、これらの関数がもはやパッケージの外部に公開されないことを意味します。これにより、net/http
パッケージの公開APIがスリム化され、内部的なチャンク処理の実装詳細が隠蔽されます。これは、パッケージの凝集度を高め、外部からの不適切な利用を防ぐための典型的なリファクタリング手法です。
-
net/http/httputil/chunked.go
の新規追加:- このファイルでは、
net/http
から移動されたNewChunkedWriter
とNewChunkedReader
の公開バージョンが提供されます。これにより、ユーザーは引き続きこれらのチャンク処理ユーティリティを利用できますが、その場所がhttputil
パッケージに変わったことで、これらの機能がコアHTTPプロトコルの一部ではなく、ユーティリティであることを明確に示しています。 - 特に
NewChunkedReader
の実装は注目に値します。これは、io.MultiReader
を使って、ダミーのHTTPリクエストヘッダーと実際のチャンクデータ、そして終端の\r\n
を結合し、それをhttp.ReadRequest
に渡すことで、http
パッケージが持つ既存のチャンクデコードロジックを再利用しています。これは、コードの重複を避け、既存の堅牢な実装を活用するための効率的なアプローチです。
- このファイルでは、
-
net/http/httputil/dump.go
の新規追加:- このファイルは、HTTPリクエストとレスポンスのワイヤー表現をダンプする機能を提供します。
DumpRequest
とDumpResponse
は、それぞれリクエストとレスポンスの生データをデバッグ目的で表示するために使用されます。DumpRequestOut
の追加は、このコミットの重要な改善点の一つです。従来のDumpRequest
は、リクエストが「受信された」形式と「送信される」形式のどちらを指すのかが不明瞭でした。DumpRequestOut
は、Goのhttp.Transport
がリクエストを送信する際に自動的に追加するヘッダー(例:User-Agent
、Host
など)を含めて、実際にネットワークに送信されるであろうリクエストの形式を再現します。これは、クライアント側のHTTP通信のデバッグにおいて非常に有用です。http.Transport
のDial
関数をオーバーライドし、ダミーのnet.Conn
を返すことで、実際にネットワーク通信を行わずにリクエストのワイヤー表現をキャプチャするという巧妙な手法が用いられています。
-
net/http/transfer.go
の変更:- このファイルは、HTTPメッセージの転送(読み書き)ロジックを扱います。チャンクエンコーディングの内部的な処理で、
net/http
パッケージ内の非公開関数であるnewChunkedWriter
とnewChunkedReader
を呼び出すように変更されました。これにより、transfer.go
はhttputil
パッケージに直接依存することなく、チャンク処理を実行できます。これは、パッケージ間の依存関係を最小限に抑え、モジュール性を高めるというGoの設計原則に沿ったものです。
- このファイルは、HTTPメッセージの転送(読み書き)ロジックを扱います。チャンクエンコーディングの内部的な処理で、
全体として、このコミットは、net/http
パッケージのコア機能をより明確にし、デバッグやユーティリティ関連の機能を適切なサブパッケージに移動させることで、Goの標準ライブラリの構造と保守性を向上させています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
net/http
パッケージのドキュメント: https://pkg.go.dev/net/httpnet/http/httputil
パッケージのドキュメント: https://pkg.go.dev/net/http/httputil- Goのコードレビューシステム (Gerrit): https://golang.org/cl/5339041 (元の変更リスト)
参考にした情報源リンク
- Go言語の公式ドキュメントおよびパッケージドキュメント
- Go言語のソースコード(特に
net/http
およびnet/http/httputil
パッケージ) - HTTP/1.1仕様 (RFC 2616, 特にチャンク転送エンコーディングに関するセクション)
- Go言語のコミット履歴と関連する議論(Gerritの変更リストコメントなど)
- Go言語のHTTPパッケージに関する一般的な解説記事やチュートリアル