Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)に分離するという方針を指します。

具体的には、以下の点が挙げられます。

  1. 責務の分離: DumpRequestDumpResponseのようなデバッグ・診断目的の関数、およびNewChunkedReader/NewChunkedWriterのような低レベルのチャンクエンコーディング処理は、HTTPプロトコルの基本的な動作に必須ではありません。これらをhttputil(HTTPユーティリティ)パッケージに移動することで、net/httpパッケージはHTTPプロトコルのコア機能に集中できるようになります。
  2. APIの明確化と混乱の解消: DumpRequestは、リクエストの「ワイヤー表現」(ネットワーク上を流れる形式)をダンプする機能を提供しますが、これが「受信したリクエスト」と「送信するリクエスト」のどちらを指すのかが不明瞭で、ユーザーからの混乱やバグレポートの原因となっていました。このコミットでは、特に「送信するリクエスト」を明確にダンプするためのDumpRequestOut関数が追加され、APIの意図がより明確になりました。
  3. パッケージの保守性向上: コアパッケージが肥大化すると、その保守や変更が困難になります。関連性の低い機能を分離することで、各パッケージのコードベースが小さくなり、理解しやすさ、テストのしやすさ、そして将来的な変更の容易さが向上します。

前提知識の解説

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)を含めて、送信されるリクエストのワイヤー表現をダンプします。

技術的詳細

このコミットは、主に以下の技術的な変更を含んでいます。

  1. ファイルの移動と削除:
    • 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)に変更されました。
  2. httputilパッケージへの機能追加:
    • httputil/dump.goには、既存のDumpRequestDumpResponseに加えて、新たにDumpRequestOutが追加されました。DumpRequestOutは、http.Transportが内部的にリクエストを処理するメカニズムを模倣し、User-Agentなどの自動的に追加されるヘッダーを含んだ形で送信リクエストをダンプできるように設計されています。これは、実際のネットワーク通信に近い形でリクエストを再現するために重要です。
    • httputil/chunked.goには、NewChunkedWriterNewChunkedReaderの公開バージョンが実装されました。特にNewChunkedReaderの実装は興味深く、io.MultiReaderhttp.ReadRequestを組み合わせて、チャンクエンコードされたストリームをHTTPリクエストボディとして「偽装」することで、既存のhttpパッケージのチャンクデコードロジックを再利用しています。これは、コードの重複を避けるための巧妙なハックとコメントされています。
  3. テストファイルの移動と追加:
    • httputil/chunked_test.gohttputil/dump_test.goが新規作成され、移動された機能のテストが追加されました。これにより、機能が正しく動作することを確認し、将来の変更に対する回帰を防ぎます。
    • net/http/requestwrite_test.goからは、DumpRequestに関連するテストケースが削除されました。これは、DumpRequestnet/httpパッケージから移動されたためです。
  4. 依存関係の更新:
    • src/pkg/net/http/Makefilesrc/pkg/net/http/httputil/Makefileが更新され、ファイルの移動と追加がビルドシステムに反映されました。
    • src/pkg/net/http/request.goからdumpWriteメソッドとreqWriteExcludeHeaderDumpマップが削除されました。これは、DumpRequestのロジックがhttputilに移動したためです。
    • src/pkg/net/http/transfer.goでは、チャンクエンコーディングの内部的な利用箇所で、NewChunkedWriterNewChunkedReaderの代わりに、非公開化されたnewChunkedWriternewChunkedReaderが呼び出されるように変更されました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集約されます。

  1. src/pkg/net/http/chunked.go:

    • NewChunkedWriterNewChunkedReaderが非公開のnewChunkedWriternewChunkedReaderにリネームされ、外部から直接呼び出せなくなりました。
    --- 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}
     }
    
  2. src/pkg/net/http/dump.go:

    • ファイル全体が削除されました。
  3. src/pkg/net/http/httputil/chunked.go (新規ファイル):

    • NewChunkedWriterNewChunkedReaderの公開バージョンが実装されました。特に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
    }
    
  4. src/pkg/net/http/httputil/dump.go (新規ファイル):

    • DumpRequestDumpResponse、そして新しく追加された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
    }
    
  5. src/pkg/net/http/transfer.go:

    • 内部的なチャンクエンコーディングの処理で、NewChunkedWriterNewChunkedReaderの代わりに、非公開化されたnewChunkedWriternewChunkedReaderが使用されるように変更されました。
    --- 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の変更:

    • NewChunkedWriterNewChunkedReadernewChunkedWriternewChunkedReaderにリネームされたことは、これらの関数がもはやパッケージの外部に公開されないことを意味します。これにより、net/httpパッケージの公開APIがスリム化され、内部的なチャンク処理の実装詳細が隠蔽されます。これは、パッケージの凝集度を高め、外部からの不適切な利用を防ぐための典型的なリファクタリング手法です。
  • net/http/httputil/chunked.goの新規追加:

    • このファイルでは、net/httpから移動されたNewChunkedWriterNewChunkedReaderの公開バージョンが提供されます。これにより、ユーザーは引き続きこれらのチャンク処理ユーティリティを利用できますが、その場所がhttputilパッケージに変わったことで、これらの機能がコアHTTPプロトコルの一部ではなく、ユーティリティであることを明確に示しています。
    • 特にNewChunkedReaderの実装は注目に値します。これは、io.MultiReaderを使って、ダミーのHTTPリクエストヘッダーと実際のチャンクデータ、そして終端の\r\nを結合し、それをhttp.ReadRequestに渡すことで、httpパッケージが持つ既存のチャンクデコードロジックを再利用しています。これは、コードの重複を避け、既存の堅牢な実装を活用するための効率的なアプローチです。
  • net/http/httputil/dump.goの新規追加:

    • このファイルは、HTTPリクエストとレスポンスのワイヤー表現をダンプする機能を提供します。
    • DumpRequestDumpResponseは、それぞれリクエストとレスポンスの生データをデバッグ目的で表示するために使用されます。
    • DumpRequestOutの追加は、このコミットの重要な改善点の一つです。従来のDumpRequestは、リクエストが「受信された」形式と「送信される」形式のどちらを指すのかが不明瞭でした。DumpRequestOutは、Goのhttp.Transportがリクエストを送信する際に自動的に追加するヘッダー(例: User-AgentHostなど)を含めて、実際にネットワークに送信されるであろうリクエストの形式を再現します。これは、クライアント側のHTTP通信のデバッグにおいて非常に有用です。http.TransportDial関数をオーバーライドし、ダミーのnet.Connを返すことで、実際にネットワーク通信を行わずにリクエストのワイヤー表現をキャプチャするという巧妙な手法が用いられています。
  • net/http/transfer.goの変更:

    • このファイルは、HTTPメッセージの転送(読み書き)ロジックを扱います。チャンクエンコーディングの内部的な処理で、net/httpパッケージ内の非公開関数であるnewChunkedWriternewChunkedReaderを呼び出すように変更されました。これにより、transfer.gohttputilパッケージに直接依存することなく、チャンク処理を実行できます。これは、パッケージ間の依存関係を最小限に抑え、モジュール性を高めるというGoの設計原則に沿ったものです。

全体として、このコミットは、net/httpパッケージのコア機能をより明確にし、デバッグやユーティリティ関連の機能を適切なサブパッケージに移動させることで、Goの標準ライブラリの構造と保守性を向上させています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびパッケージドキュメント
  • Go言語のソースコード(特にnet/httpおよびnet/http/httputilパッケージ)
  • HTTP/1.1仕様 (RFC 2616, 特にチャンク転送エンコーディングに関するセクション)
  • Go言語のコミット履歴と関連する議論(Gerritの変更リストコメントなど)
  • Go言語のHTTPパッケージに関する一般的な解説記事やチュートリアル