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

[インデックス 13406] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HTTPリクエストのPOSTまたはPUTボディから送信されたフォームデータへのアクセス方法を改善するものです。具体的には、URLクエリパラメータとPOSTボディパラメータを明確に区別し、POSTボディパラメータに優先順位を与えるための Request 構造体への PostForm フィールドの追加と、関連する解析ロジックおよびアクセサメソッドの導入が行われました。これにより、開発者はPOSTボディ専用のフォーム値に直接アクセスできるようになり、より柔軟なフォームデータ処理が可能になります。

コミット

commit abb3c0618b658a41bf91a087f1737412e93ff6d9
Author: Patrick Mylund Nielsen <patrick@patrickmn.com>
Date:   Mon Jun 25 20:41:46 2012 -0400

    net/http: provide access to POST-only form values
    
    Fixes #3630.
    
    R=rsc
    CC=bradfitz, dsymonds, golang-dev, rodrigo.moraes
    https://golang.org/cl/6210067

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/abb3c0618b658a41bf91a087f1737412e93ff6d9

元コミット内容

net/http: provide access to POST-only form values

Fixes #3630.

R=rsc
CC=bradfitz, dsymonds, golang-dev, rodrigo.moraes
https://golang.org/cl/6210067

変更の背景

この変更は、Goの net/http パッケージにおける Request.Form および Request.FormValue の既存の挙動に関する課題(Issue #3630)に対応するために行われました。以前のバージョンでは、Request.Form はURLのクエリパラメータとPOST/PUTリクエストのボディパラメータの両方を結合して保持していました。また、FormValue メソッドは、同じ名前のパラメータがURLクエリとPOSTボディの両方に存在する場合、どちらの値を返すかについて明確な優先順位がありませんでした。

特に問題だったのは、POSTリクエストのボディは一度しか読み取れないため、FormValue がPOSTボディを読み取ってしまうと、その後の処理で生のPOSTボディにアクセスすることが困難になる場合があるという点です。開発者は、URLクエリパラメータとPOSTボディパラメータを個別に、または特定の優先順位で扱いたいというニーズを持っていました。

このコミットは、POST/PUTリクエストのボディからのみ解析されたフォームデータを保持する新しいフィールド PostForm を導入することで、この問題を解決しようとしました。これにより、開発者はPOSTボディ専用のデータに明確にアクセスできるようになり、Form フィールドは引き続きURLクエリとPOSTボディの結合されたビューを提供しつつ、POSTボディのデータに優先順位を与えるように変更されました。

前提知識の解説

HTTPリクエストのフォームデータ

Webアプリケーションでは、ユーザーからの入力をサーバーに送信するためにフォームが使用されます。フォームデータは主に以下の2つの方法で送信されます。

  1. URLクエリパラメータ: GETリクエストで主に使用され、URLの ? の後に key=value 形式で追加されます(例: http://example.com/search?q=golang&page=1)。
  2. リクエストボディ: POSTやPUTリクエストで主に使用され、HTTPリクエストのボディ内にデータが含まれます。一般的な Content-Type は以下の通りです。
    • application/x-www-form-urlencoded: キーと値のペアが & で区切られ、URLエンコードされた形式で送信されます(例: name=John+Doe&age=30)。これはHTMLフォームのデフォルトのエンコーディングです。
    • multipart/form-data: ファイルアップロードを含むフォームデータに使用されます。各フィールドは異なるパートとして送信され、それぞれが独自の Content-Type を持つことができます。

Go net/http パッケージの Request 構造体

Goの net/http パッケージは、HTTPクライアントとサーバーを実装するための機能を提供します。http.Request 構造体は、受信したHTTPリクエストのすべての側面(メソッド、URL、ヘッダー、ボディなど)を表します。

  • Request.URL: リクエストのURLを url.URL 型で保持します。URL.RawQuery はURLのクエリ文字列をそのまま保持します。
  • Request.Body: リクエストボディを表す io.ReadCloser インターフェースです。ボディは一度しか読み取ることができません。
  • Request.Header: リクエストヘッダーを http.Header 型(map[string][]string のエイリアス)で保持します。Content-Type ヘッダーは、リクエストボディのメディアタイプを示します。
  • Request.Form: url.Values 型(map[string][]string のエイリアス)で、URLクエリパラメータとPOST/PUTボディパラメータを結合したものを保持します。このフィールドは ParseForm メソッドが呼び出された後に利用可能になります。
  • Request.ParseForm(): リクエストのURLクエリと、POST/PUTリクエストの場合はボディを解析し、結果を Request.Form に格納します。application/x-www-form-urlencodedmultipart/form-data の両方を処理できます。
  • Request.FormValue(key string): ParseForm を自動的に呼び出し、指定されたキーに対応するフォーム値の最初のものを返します。

url.Values

url.Valuesmap[string][]string のエイリアスで、URLクエリパラメータやフォームデータをキーと値のリストとして表現するために使用されます。例えば、q=foo&q=bar のようなクエリは {"q": ["foo", "bar"]} として表現されます。

技術的詳細

このコミットの主要な変更点は、http.Request 構造体に新しいフィールド PostForm を導入し、フォームデータの解析ロジックを再構築したことです。

  1. Request.PostForm フィールドの追加:

    • Request 構造体に PostForm url.Values という新しいフィールドが追加されました。
    • このフィールドは、POSTまたはPUTリクエストのボディパラメータから解析されたフォームデータのみを保持します。
    • ParseForm が呼び出された後にのみ利用可能になります。
    • Form フィールドと同様に、HTTPクライアントは PostForm を無視し、代わりに Body を使用します。
  2. parsePostForm ヘルパー関数の導入:

    • func parsePostForm(r *Request) (vs url.Values, err error) という新しい内部ヘルパー関数が追加されました。
    • この関数は、リクエストの BodyContent-Type ヘッダーを基に、POST/PUTリクエストのボディからフォームデータを解析する責任を負います。
    • application/x-www-form-urlencoded 形式のボディを処理し、最大10MBのボディサイズ制限を適用します。
    • multipart/form-dataParseMultipartForm によって処理されるため、ここでは直接処理されません。
  3. copyValues ヘルパー関数の導入:

    • func copyValues(dst, src url.Values) というシンプルなヘルパー関数が追加されました。
    • これは、src のすべてのキーと値を dst にコピーするために使用されます。
  4. Request.ParseForm() メソッドの変更:

    • ParseForm のロジックが大幅に変更されました。
    • まず、r.PostForm がまだ解析されていない場合、POSTまたはPUTリクエストであれば parsePostForm を呼び出して r.PostForm を設定します。
    • 次に、r.Form がまだ解析されていない場合、r.PostForm の内容を r.Form にコピーします。
    • その後、URLクエリパラメータを解析し、その値を r.Form に追加します。ここで重要なのは、copyValues を使用して r.Form に追加されるため、POSTボディパラメータがURLクエリパラメータよりも優先されるという点です。つまり、同じキーが存在する場合、POSTボディの値が r.Form に先に格納され、URLクエリの値はその後に追加されます。
    • これにより、FormValue はPOSTボディの値を優先的に返すようになります。
  5. Request.PostFormValue(key string) メソッドの追加:

    • func (r *Request) PostFormValue(key string) string という新しいアクセサメソッドが追加されました。
    • このメソッドは、指定されたキーに対応するPOSTまたはPUTリクエストボディからのフォーム値のみを返します。URLクエリパラメータは無視されます。
    • ParseMultipartForm または ParseForm を必要に応じて呼び出します。

これらの変更により、開発者は Request.Form を使用してURLクエリとPOSTボディの結合されたビュー(POSTボディ優先)にアクセスできるだけでなく、Request.PostForm を使用してPOSTボディ専用のデータにアクセスできるようになりました。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/net/http/request.go: http.Request 構造体の定義と、フォーム解析に関連するメソッドが変更されました。
  2. src/pkg/net/http/request_test.go: 変更されたフォーム解析ロジックを検証するためのテストケースが追加・修正されました。

src/pkg/net/http/request.go の変更点

  • Request 構造体に PostForm url.Values フィールドが追加されました。
  • copyValues 関数が追加されました。
  • parsePostForm 関数が追加されました。
  • ParseForm メソッドのロジックが大幅に書き換えられ、PostForm の解析と Form へのマージロジックが導入されました。
  • FormValue メソッドのコメントが更新され、POST/PUTボディパラメータがURLクエリパラメータよりも優先されることが明記されました。
  • PostFormValue メソッドが追加されました。

src/pkg/net/http/request_test.go の変更点

  • TestPostQuery テストケースが修正され、PostForm の挙動と、Form がPOSTボディの値を優先することを確認するアサーションが追加されました。
  • TestParseFormInitializeOnError テストケースが追加され、ParseForm がエラー時でも FormPostForm を初期化することを確認します。

コアとなるコードの解説

src/pkg/net/http/request.go

// Request struct
type Request struct {
    // ... 既存のフィールド ...
    Form url.Values

    // PostForm contains the parsed form data from POST or PUT
    // body parameters.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores PostForm and uses Body instead.
    PostForm url.Values

    // ... 既存のフィールド ...
}

// copyValues: url.Values をコピーするヘルパー関数
func copyValues(dst, src url.Values) {
    for k, vs := range src {
        for _, value := range vs {
            dst.Add(k, value)
        }
    }
}

// parsePostForm: POST/PUTボディからフォームデータを解析する内部関数
func parsePostForm(r *Request) (vs url.Values, err error) {
    if r.Body == nil {
        err = errors.New("missing form body")
        return
    }
    ct := r.Header.Get("Content-Type")
    ct, _, err = mime.ParseMediaType(ct)
    switch {
    case ct == "application/x-www-form-urlencoded":
        var reader io.Reader = r.Body
        maxFormSize := int64(1<<63 - 1)
        if _, ok := r.Body.(*maxBytesReader); !ok {
            maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
            reader = io.LimitReader(r.Body, maxFormSize+1)
        }
        b, e := ioutil.ReadAll(reader)
        if e != nil {
            if err == nil {
                err = e
            }
            break
        }
        if int64(len(b)) > maxFormSize {
            err = errors.New("http: POST too large")
            return
        }
        vs, e = url.ParseQuery(string(b))
        if err == nil {
            err = e
        }
    case ct == "multipart/form-data":
        // handled by ParseMultipartForm (which is calling us, or should be)
    }
    return
}

// ParseForm: フォーム解析ロジックの変更
func (r *Request) ParseForm() (err error) {
    if r.PostForm == nil { // PostForm がまだ解析されていない場合
        if r.Method == "POST" || r.Method == "PUT" {
            r.PostForm, err = parsePostForm(r) // parsePostForm を呼び出して解析
        }
        if r.PostForm == nil {
            r.PostForm = make(url.Values) // nil の場合は空の url.Values を作成
        }
    }

    if r.Form == nil { // Form がまだ解析されていない場合
        if len(r.PostForm) > 0 {
            r.Form = make(url.Values)
            copyValues(r.Form, r.PostForm) // PostForm の値を Form にコピー (優先)
        }
        var newValues url.Values
        if r.URL != nil {
            var e error
            newValues, e = url.ParseQuery(r.URL.RawQuery) // URLクエリを解析
            if err == nil {
                err = e
            }
        }
        if newValues == nil {
            newValues = make(url.Values)
        }
        if r.Form == nil {
            r.Form = newValues
        } else {
            copyValues(r.Form, newValues) // URLクエリの値を Form に追加
        }
    }
    return err
}

// PostFormValue: POSTボディ専用のフォーム値を取得する新しいメソッド
func (r *Request) PostFormValue(key string) string {
    if r.PostForm == nil {
        r.ParseMultipartForm(defaultMaxMemory) // 必要に応じて解析をトリガー
    }
    if vs := r.PostForm[key]; len(vs) > 0 {
        return vs[0]
    }
    return ""
}

src/pkg/net/http/request_test.go

// TestPostQuery: POSTリクエストのフォーム解析のテストケース
func TestPostQuery(t *testing.T) {
    // URLクエリとPOSTボディの両方に同じキー 'both' が存在し、
    // 'prio' と 'empty' も両方に存在するケースをテスト
    req, _ := NewRequest("POST", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&empty=not",
        strings.NewReader("z=post&both=y&prio=2&empty="))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")

    // FormValue は POST ボディの値を優先することを確認
    if q := req.FormValue("q"); q != "foo" {
        t.Errorf(`req.FormValue("q") = %q, want "foo"`, q)
    }
    if z := req.FormValue("z"); z != "post" {
        t.Errorf(`req.FormValue("z") = %q, want "post"`, z)
    }

    // PostForm には URL クエリの 'q' は含まれないことを確認
    if bq, found := req.PostForm["q"]; found {
        t.Errorf(`req.PostForm["q"] = %q, want no entry in map`, bq)
    }
    // PostFormValue は POST ボディの 'z' を返すことを確認
    if bz := req.PostFormValue("z"); bz != "post" {
        t.Errorf(`req.PostFormValue("z") = %q, want "post"`, bz)
    }

    // Form["q"] は URL クエリの値を保持することを確認
    if qs := req.Form["q"]; !reflect.DeepEqual(qs, []string{"foo", "bar"}) {
        t.Errorf(`req.Form["q"] = %q, want ["foo", "bar"]`, qs)
    }
    // Form["both"] は POST ボディの値を優先し、その後に URL クエリの値が続くことを確認
    if both := req.Form["both"]; !reflect.DeepEqual(both, []string{"y", "x"}) {
        t.Errorf(`req.Form["both"] = %q, want ["y", "x"]`, both)
    }
    // FormValue("prio") は POST ボディの値を優先することを確認
    if prio := req.FormValue("prio"); prio != "2" {
        t.Errorf(`req.FormValue("prio") = %q, want "2" (from body)`, prio)
    }
    // FormValue("empty") は POST ボディの空の値を優先することを確認
    if empty := req.FormValue("empty"); empty != "" {
        t.Errorf(`req.FormValue("empty") = %q, want "" (from body)`, empty)
    }
}

// TestParseFormInitializeOnError: ParseForm がエラー時でも Form と PostForm を初期化することを確認
func TestParseFormInitializeOnError(t *testing.T) {
    nilBody, _ := NewRequest("POST", "http://www.google.com/search?q=foo", nil)
    tests := []*Request{
        nilBody,
        {Method: "GET", URL: nil},
    }
    for i, req := range tests {
        err := req.ParseForm() // エラーが発生するケース
        if req.Form == nil {
            t.Errorf("%d. Form not initialized, error %v", i, err)
        }
        if req.PostForm == nil {
            t.Errorf("%d. PostForm not initialized, error %v", i, err)
        }
    }
}

関連リンク

参考にした情報源リンク