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

[インデックス 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の基本的なサポートは持っていたものの、その機能に関する公式ドキュメントがほとんど存在しませんでした。これにより、開発者がこの機能を適切に利用したり、その挙動を理解したりすることが困難でした。

このコミットの背景には、以下の課題認識があったと考えられます。

  1. ドキュメントの不足: 機能は存在するものの、その使い方やセマンティクスが明確に記述されていないため、利用者が混乱する可能性がありました。
  2. コードの複雑性/未整理: 長期間にわたって開発されてきたコードベースには、改善の余地がある部分や、将来的な拡張を妨げるような未整理な部分が存在することがあります。特に、HTTP TrailerのようなHTTPプロトコルの特定の側面を扱うコードは、その仕様の複雑さから、注意深い設計と実装が求められます。
  3. 潜在的なバグ: ドキュメントが不十分なコードは、テストも不十分である可能性があり、潜在的なバグを抱えていることがあります。
  4. 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.Readerio.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 でも最終的な値でも構いません。
    • チャンク転送リクエストを送信するためには、ContentLength0 または -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 の場合は新しいマップで初期化し、そうでない場合は既存のマップに新しいトレーラーをマージする処理がより安全かつ明確に行われるようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

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

    • Request 構造体の Trailer フィールドに関するコメントが大幅に加筆・修正されました。これは、このコミットの主要な目的であるドキュメント改善の核心部分です。
    • ReadRequest 関数内のいくつかの TODO コメントが削除されました。
    • readTrailer 関数内で、mergeSetHeader ヘルパー関数が導入され、トレーラーのマージロジックが改善されました。
  2. src/pkg/net/http/transfer.go:

    • transferWriter.WriteHeader メソッドにおいて、Trailer ヘッダーの生成ロジックが改善され、トレーラーキーのソートと不正なキーのチェックが追加されました。
    • transferWriter.WriteBody メソッドにおいて、チャンク転送エンコーディングの場合に実際にトレーラーを書き込むロジックが追加されました。以前は TODO コメントで示されていました。
    • fixTrailer 関数内で、トレーラーキーの処理方法が変更されました(trailer.Del(key) から trailer[key] = nil へ)。
  3. src/pkg/net/http/client_test.go:

    • TestClientTrailers という新しいテスト関数が追加されました。このテストは、クライアントとサーバーの両方でHTTP Trailerの送受信が正しく行われることを検証します。これには、eofReaderFunc というカスタム io.Reader の実装も含まれます。

コアとなるコードの解説

src/pkg/net/http/request.goRequest.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.gotransferWriter.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.gotransferWriter.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.goTestClientTrailers

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.MultiReadereofReaderFunc を組み合わせて、リクエストボディの読み取り中に req.Trailer の値を動的に設定している点です。これは、トレーラーの値がボディの生成中に決定されるという実際のユースケースをシミュレートしています。req.ContentLength = -1 は、GoのHTTPクライアントがチャンク転送エンコーディングを使用するように指示する重要な設定です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • HTTP/1.1 RFC 2616
  • Go言語のコミット履歴と関連するGerritチェンジリスト
  • 一般的なHTTPプロトコルに関する知識