[インデックス 19103] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http パッケージにおけるHTTP Trailer(トレーラー)のサポートに関するものです。具体的には、http.Request 構造体の Trailer フィールドのドキュメントを改善し、関連するコードのクリーンアップ、バグ修正、およびテストの追加を行っています。
コミット
commit 9b3e2aa1dbfdb98f634dacf0cbca802221af1f36
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Apr 10 17:01:21 2014 -0700
net/http: document, test, define, clean up Request.Trailer
Go's had pretty decent HTTP Trailer support for a long time, but
the docs have been largely non-existent. Fix that.
In the process, re-learn the Trailer code, clean some stuff
up, add some error checks, remove some TODOs, fix a minor bug
or two, and add tests.
LGTM=adg
R=golang-codereviews, adg
CC=dsymonds, golang-codereviews, rsc
https://golang.org/cl/86660043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b3e2aa1dbfdb98f634dacf0cbca802221af1f36
元コミット内容
このコミットの目的は、Goの net/http パッケージにおけるHTTP Trailerのサポートを改善することです。具体的には、以下の点が挙げられています。
- ドキュメントの追加と改善: 既存のHTTP Trailerサポートに関するドキュメントが不足していたため、これを修正します。
- コードのクリーンアップ: Trailer関連のコードを再学習し、整理します。
- エラーチェックの追加: 堅牢性を高めるためにエラーチェックを追加します。
- TODOコメントの削除: コード内の未完了タスクを示すTODOコメントを削除します。
- バグ修正: 1つまたは2つの小さなバグを修正します。
- テストの追加: Trailer機能の動作を検証するためのテストを追加します。
変更の背景
Goの net/http パッケージは、以前からHTTP Trailerの基本的なサポートは持っていたものの、その機能に関する公式ドキュメントがほとんど存在しませんでした。これにより、開発者がこの機能を適切に利用したり、その挙動を理解したりすることが困難でした。
このコミットの背景には、以下の課題認識があったと考えられます。
- ドキュメントの不足: 機能は存在するものの、その使い方やセマンティクスが明確に記述されていないため、利用者が混乱する可能性がありました。
- コードの複雑性/未整理: 長期間にわたって開発されてきたコードベースには、改善の余地がある部分や、将来的な拡張を妨げるような未整理な部分が存在することがあります。特に、HTTP TrailerのようなHTTPプロトコルの特定の側面を扱うコードは、その仕様の複雑さから、注意深い設計と実装が求められます。
- 潜在的なバグ: ドキュメントが不十分なコードは、テストも不十分である可能性があり、潜在的なバグを抱えていることがあります。
- TODOの解消: 開発中に一時的に追加されたTODOコメントは、その機能が完成した際に削除されるべきものです。これらが残っていると、コードの品質や保守性を示す指標となります。
これらの課題に対処し、net/http パッケージの品質と使いやすさを向上させることが、このコミットの主要な動機となっています。特に、HTTP Trailerは特定の高度なHTTP通信シナリオで重要となるため、そのサポートを明確化し、堅牢にすることは、ライブラリ全体の信頼性向上に寄与します。
前提知識の解説
このコミットを理解するためには、以下のHTTPおよびGo言語に関する前提知識が必要です。
1. HTTP Trailer (トレーラー)
HTTP Trailerは、HTTP/1.1の「チャンク転送エンコーディング (Chunked Transfer Encoding)」と密接に関連する概念です。通常、HTTPヘッダーはメッセージボディの前に送信されますが、チャンク転送エンコーディングを使用する場合、メッセージボディの後に追加のヘッダー(トレーラー)を送信することができます。
- チャンク転送エンコーディング: メッセージボディのサイズが事前に不明な場合や、動的に生成される場合に用いられる転送メカニズムです。ボディは複数の「チャンク」に分割され、各チャンクはサイズ情報と共に送信されます。ボディの終端は、サイズが0のチャンクで示されます。
- Trailerヘッダー: クライアントまたはサーバーが、メッセージボディの後にどのヘッダーを送信するかを事前に示すために使用します。これは、通常のヘッダーセクション(メッセージボディの前)に含まれます。例えば、
Trailer: Content-MD5, X-Custom-Trailerのように指定します。 - トレーラーの用途:
- コンテンツの完全性チェック: メッセージボディ全体のハッシュ値(例:
Content-MD5)を計算し、ボディの送信後にトレーラーとして送信することで、受信側がデータの完全性を検証できます。 - デジタル署名: ボディの内容に対するデジタル署名をトレーラーとして送信することで、メッセージの認証と非改ざん性を保証できます。
- 動的なメタデータ: ボディの生成中にしか決定できないようなメタデータ(例: 処理時間、最終的なステータスコードなど)を送信するために使用されます。
- コンテンツの完全性チェック: メッセージボディ全体のハッシュ値(例:
注意点: HTTP Trailerは、すべてのHTTPクライアント、サーバー、プロキシで完全にサポートされているわけではありません。特にプロキシは、ボディをバッファリングしない限り、トレーラーを適切に処理できない場合があります。
2. Go言語の net/http パッケージ
Goの net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。
http.Request構造体: HTTPリクエストを表す構造体です。クライアントがサーバーに送信するリクエスト、またはサーバーがクライアントから受信するリクエストのいずれかを表します。Header: リクエストヘッダー(map[string][]string型)を格納します。Body: リクエストボディを表すio.ReadCloserインターフェースです。Trailer: このコミットで焦点が当てられているフィールドで、HTTP Trailerを格納するためのhttp.Header型のマップです。
http.Response構造体: HTTPレスポンスを表す構造体です。Trailer: レスポンスのHTTP Trailerを格納します。
http.Handlerインターフェース: HTTPサーバーがリクエストを処理するためのインターフェースです。ServeHTTP(ResponseWriter, *Request)メソッドを持ちます。http.ResponseWriterインターフェース: HTTPサーバーがクライアントにレスポンスを書き込むためのインターフェースです。Header(): レスポンスヘッダーを取得します。Write([]byte): レスポンスボディを書き込みます。WriteHeader(statusCode int): ステータスコードを書き込みます。
io.Readerとio.Writer: Goの標準的なI/Oインターフェースです。net/httpパッケージでは、リクエスト/レスポンスボディの読み書きにこれらが広く利用されます。
3. io.MultiReader
io.MultiReader は、複数の io.Reader を連結し、それらを単一の io.Reader として扱うためのGoの関数です。このコミットのテストコードで、リクエストボディとトレーラーを送信する際に、ボディの途中でトレーラーを設定するロジックをシミュレートするために使用されています。
4. reflect.DeepEqual
reflect.DeepEqual は、Goの reflect パッケージに含まれる関数で、2つの値が「深く」等しいかどうかを比較します。これは、スライス、マップ、構造体などの複合型を比較する際に特に有用です。このコミットのテストで、期待されるトレーラーと実際に受信したトレーラーが一致するかどうかを検証するために使用されています。
技術的詳細
このコミットは、net/http パッケージにおける Request.Trailer の挙動を明確にし、その実装を改善することに焦点を当てています。
Request.Trailer のドキュメント改善
最も重要な変更点の一つは、http.Request 構造体内の Trailer フィールドのコメントが大幅に加筆・修正されたことです。
変更前(抜粋):
// Trailer maps trailer keys to values. Like for Header, if the
// response has multiple trailer lines with the same key, they will be
// concatenated, delimited by commas.
// For server requests Trailer is only populated after Body has been
// closed or fully consumed.
// Trailer support is only partially complete.
このコメントは非常に簡潔で、「部分的にしか完了していない」という記述があり、利用者に混乱を与える可能性がありました。
変更後(抜粋):
// Trailer specifies additional headers that are sent after the request
// body.
//
// For server requests the Trailer map initially contains only the
// trailer keys, with nil values. (The client declares which trailers it
// will later send.) While the handler is reading from Body, it must
// not reference Trailer. After reading from Body returns EOF, Trailer
// can be read again and will contain non-nil values, if they were sent
// by the client.
//
// For client requests Trailer must be initialized to a map containing
// the trailer keys to later send. The values may be nil or their final
// values. The ContentLength must be 0 or -1, to send a chunked request.
// After the HTTP request is sent the map values can be updated while
// the request body is read. Once the body returns EOF, the caller must
// not mutate Trailer.
//
// Few HTTP clients, servers, or proxies support HTTP trailers.
新しいドキュメントは、サーバー側とクライアント側の両方で Trailer フィールドがどのように機能するかを詳細に説明しています。
- サーバーリクエストの場合:
Trailerマップは最初、クライアントが送信を宣言したトレーラーキーのみを含み、値はnilになります。- ハンドラが
Bodyを読み取っている間はTrailerを参照してはなりません。 Bodyの読み取りがEOFを返した後、Trailerは再度読み取ることができ、クライアントから送信された場合はnilではない値が含まれます。
- クライアントリクエストの場合:
Trailerは、後で送信するトレーラーキーを含むマップとして初期化されなければなりません。値はnilでも最終的な値でも構いません。- チャンク転送リクエストを送信するためには、
ContentLengthは0または-1でなければなりません。 - HTTPリクエストが送信された後、リクエストボディが読み取られている間はマップの値を更新できます。
- ボディが
EOFを返した後、呼び出し元はTrailerを変更してはなりません。
また、「Few HTTP clients, servers, or proxies support HTTP trailers.」という重要な注意書きが追加され、トレーラーの利用における現実的な制約を明示しています。
transfer.go の変更
transfer.go は、HTTPメッセージの転送エンコーディング(チャンク転送など)を処理する内部ロジックを含んでいます。
transferWriter.WriteHeader:Trailerヘッダーの書き込みロジックが改善されました。以前はTODOコメントがあり、トレーラーキーをカンマ区切りで連結する際に、より良いアロケーション方法があることが示唆されていました。- このコミットでは、
t.Trailerのキーをソートし、strings.Joinを使用してTrailerヘッダーを生成するように変更されています。これにより、トレーラーキーの順序が安定し、よりクリーンなコードになっています。 "Transfer-Encoding","Trailer","Content-Length"がトレーラーキーとして使用できないことのチェックが追加されました。これらはHTTPプロトコル上、トレーラーとして許可されていないヘッダーです。
transferWriter.WriteBody:- チャンク転送エンコーディングの場合に、トレーラーヘッダーを実際に書き込むロジックが追加されました。以前は
TODO(petar): Place trailer writer code here.というコメントがあり、トレーラーの書き込みが未実装でした。 t.Trailer.Write(w)を呼び出すことで、Trailerマップの内容がHTTPヘッダー形式で書き込まれるようになりました。
- チャンク転送エンコーディングの場合に、トレーラーヘッダーを実際に書き込むロジックが追加されました。以前は
client_test.go のテスト追加
TestClientTrailers という新しいテスト関数が client_test.go に追加されました。このテストは、クライアントとサーバーの両方でHTTP Trailerが正しく機能することを確認します。
- サーバー側の設定:
httptest.NewServerを使用してテスト用のHTTPサーバーを起動します。- サーバーは、レスポンスヘッダーに
Trailer: Server-Trailer-A, Server-Trailer-B, Server-Trailer-Cを設定し、クライアントにどのトレーラーを送信するかを宣言します。 - リクエストボディを読み取った後、サーバーは
Hijack()を使用してコネクションを乗っ取り、手動でチャンク転送の終端(0\r\n)とトレーラー(Server-Trailer-A: valuea,Server-Trailer-C: valuec)を書き込みます。これは、当時のGoのnet/httpパッケージでは、サーバーがトレーラーを自動的に送信する直接的なAPIがなかったため、テストのために低レベルな操作を行っています。
- クライアント側の設定:
http.NewRequestを使用してPOSTリクエストを作成します。req.Trailerを初期化し、クライアントが送信するトレーラーキー(Client-Trailer-A,Client-Trailer-B)を宣言します。io.MultiReaderとカスタムのeofReaderFuncを使用して、リクエストボディの読み取り中にreq.Trailerの値を動的に設定するシナリオをシミュレートします。これは、ボディのストリーミング中にトレーラーの値を決定できるというHTTP Trailerの特性をテストするためです。DefaultClient.Do(req)でリクエストを送信し、レスポンスを受信します。
- 検証:
- サーバーがクライアントから受信したトレーラーが正しいことを検証します。
- クライアントがサーバーから受信したトレーラーが期待される値(
Server-Trailer-A: valuea,Server-Trailer-C: valuec)と一致することをreflect.DeepEqualを使用して検証します。
request.go の変更
Request.Trailerのドキュメントが更新されました。ReadRequest関数内の不要なTODOコメント(特定のヘッダー値をパースする必要があるというもの)が削除されました。これは、このコミットがTrailerに焦点を当てており、他のヘッダーのパースは直接関係ないため、あるいは既に別の方法で処理されているためと考えられます。readTrailer関数内で、受信したトレーラーヘッダーをRequest.TrailerまたはResponse.Trailerにマージする際に、mergeSetHeaderという新しいヘルパー関数が導入されました。これにより、既存のトレーラーマップがnilの場合は新しいマップで初期化し、そうでない場合は既存のマップに新しいトレーラーをマージする処理がより安全かつ明確に行われるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/pkg/net/http/request.go:Request構造体のTrailerフィールドに関するコメントが大幅に加筆・修正されました。これは、このコミットの主要な目的であるドキュメント改善の核心部分です。ReadRequest関数内のいくつかのTODOコメントが削除されました。readTrailer関数内で、mergeSetHeaderヘルパー関数が導入され、トレーラーのマージロジックが改善されました。
-
src/pkg/net/http/transfer.go:transferWriter.WriteHeaderメソッドにおいて、Trailerヘッダーの生成ロジックが改善され、トレーラーキーのソートと不正なキーのチェックが追加されました。transferWriter.WriteBodyメソッドにおいて、チャンク転送エンコーディングの場合に実際にトレーラーを書き込むロジックが追加されました。以前はTODOコメントで示されていました。fixTrailer関数内で、トレーラーキーの処理方法が変更されました(trailer.Del(key)からtrailer[key] = nilへ)。
-
src/pkg/net/http/client_test.go:TestClientTrailersという新しいテスト関数が追加されました。このテストは、クライアントとサーバーの両方でHTTP Trailerの送受信が正しく行われることを検証します。これには、eofReaderFuncというカスタムio.Readerの実装も含まれます。
コアとなるコードの解説
src/pkg/net/http/request.go の Request.Trailer ドキュメント
// Trailer specifies additional headers that are sent after the request
// body.
//
// For server requests the Trailer map initially contains only the
// trailer keys, with nil values. (The client declares which trailers it
// will later send.) While the handler is reading from Body, it must
// not reference Trailer. After reading from Body returns EOF, Trailer
// can be read again and will contain non-nil values, if they were sent
// by the client.
//
// For client requests Trailer must be initialized to a map containing
// the trailer keys to later send. The values may be nil or their final
// values. The ContentLength must be 0 or -1, to send a chunked request.
// After the HTTP request is sent the map values can be updated while
// the request body is read. Once the body returns EOF, the caller must
// not mutate Trailer.
//
// Few HTTP clients, servers, or proxies support HTTP trailers.
Trailer Header
このドキュメントは、Request.Trailer のセマンティクスを明確に定義しています。サーバー側ではボディ読み取り完了後に値が設定され、クライアント側ではボディ送信中に値を更新できるが、EOF後は変更不可であるという重要なライフサイクル情報が提供されています。また、トレーラーのサポートが一般的ではないという注意喚起も含まれています。
src/pkg/net/http/transfer.go の transferWriter.WriteHeader
func (t *transferWriter) WriteHeader(w io.Writer) error {
// ... (既存のConnection, Content-Length, Transfer-Encodingヘッダーの書き込み) ...
// Write Trailer header
if t.Trailer != nil {
keys := make([]string, 0, len(t.Trailer))
for k := range t.Trailer {
k = CanonicalHeaderKey(k)
switch k {
case "Transfer-Encoding", "Trailer", "Content-Length":
return &badStringError{"invalid Trailer key", k}
}
keys = append(keys, k)
}
if len(keys) > 0 {
sort.Strings(keys) // トレーラーキーをソート
if _, err := io.WriteString(w, "Trailer: "+strings.Join(keys, ",")+"\\r\\n"); err != nil {
return err
}
}
}
return nil
}
このコードは、レスポンスの通常のヘッダーセクションに Trailer ヘッダーを書き込む部分です。t.Trailer に含まれるキーを抽出し、CanonicalHeaderKey で正規化し、プロトコルで禁止されているキー(Transfer-Encoding, Trailer, Content-Length)がないかチェックします。その後、キーをソートしてカンマ区切りで連結し、Trailer: key1,key2\r\n の形式でヘッダーとして書き込みます。これにより、受信側は後でどのトレーラーが来るかを事前に知ることができます。
src/pkg/net/http/transfer.go の transferWriter.WriteBody
func (t *transferWriter) WriteBody(w io.Writer) error {
var err error
var ncopy int64
// ... (ボディの書き込みロジック) ...
if chunked(t.TransferEncoding) {
// Write Trailer header
if t.Trailer != nil {
if err := t.Trailer.Write(w); err != nil { // ここで実際にトレーラーを書き込む
return err
}
}
// Last chunk, empty trailer
_, err = io.WriteString(w, "\\r\\n")
}
return err
}
この部分が、チャンク転送エンコーディングのメッセージボディの終端(最後のチャンクの後に)で、実際にトレーラーヘッダーを書き込むロジックです。t.Trailer.Write(w) を呼び出すことで、Trailer マップに格納されているヘッダーがHTTPヘッダーの形式で出力ストリームに書き込まれます。これにより、HTTP Trailerの送信が完了します。
src/pkg/net/http/client_test.go の TestClientTrailers
func TestClientTrailers(t *testing.T) {
// ... (テストサーバーのセットアップ) ...
var req *Request
req, _ = NewRequest("POST", ts.URL, io.MultiReader(
eofReaderFunc(func() {
req.Trailer["Client-Trailer-A"] = []string{"valuea"}
}),
strings.NewReader("foo"),
eofReaderFunc(func() {
req.Trailer["Client-Trailer-B"] = []string{"valueb"}
}),
))
req.Trailer = Header{
"Client-Trailer-A": nil, // to be set later
"Client-Trailer-B": nil, // to be set later
}
req.ContentLength = -1 // チャンク転送を有効にする
res, err := DefaultClient.Do(req)
// ... (レスポンスの検証) ...
}
このテストは、クライアントがトレーラーを送信し、サーバーがそれを受信し、さらにサーバーがトレーラーを送信し、クライアントがそれを受信するという双方向のシナリオを検証します。特に注目すべきは、io.MultiReader と eofReaderFunc を組み合わせて、リクエストボディの読み取り中に req.Trailer の値を動的に設定している点です。これは、トレーラーの値がボディの生成中に決定されるという実際のユースケースをシミュレートしています。req.ContentLength = -1 は、GoのHTTPクライアントがチャンク転送エンコーディングを使用するように指示する重要な設定です。
関連リンク
- HTTP/1.1 RFC 2616 - Section 14.40 Trailer: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.40
- HTTP/1.1 RFC 2616 - Section 3.6.1 Chunked Transfer Encoding: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
- Go
net/httpパッケージドキュメント: https://pkg.go.dev/net/http
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- HTTP/1.1 RFC 2616
- Go言語のコミット履歴と関連するGerritチェンジリスト
- 一般的なHTTPプロトコルに関する知識