[インデックス 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つの方法で送信されます。
- URLクエリパラメータ: GETリクエストで主に使用され、URLの
?
の後にkey=value
形式で追加されます(例:http://example.com/search?q=golang&page=1
)。 - リクエストボディ: 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-urlencoded
とmultipart/form-data
の両方を処理できます。Request.FormValue(key string)
:ParseForm
を自動的に呼び出し、指定されたキーに対応するフォーム値の最初のものを返します。
url.Values
型
url.Values
は map[string][]string
のエイリアスで、URLクエリパラメータやフォームデータをキーと値のリストとして表現するために使用されます。例えば、q=foo&q=bar
のようなクエリは {"q": ["foo", "bar"]}
として表現されます。
技術的詳細
このコミットの主要な変更点は、http.Request
構造体に新しいフィールド PostForm
を導入し、フォームデータの解析ロジックを再構築したことです。
-
Request.PostForm
フィールドの追加:Request
構造体にPostForm url.Values
という新しいフィールドが追加されました。- このフィールドは、POSTまたはPUTリクエストのボディパラメータから解析されたフォームデータのみを保持します。
ParseForm
が呼び出された後にのみ利用可能になります。Form
フィールドと同様に、HTTPクライアントはPostForm
を無視し、代わりにBody
を使用します。
-
parsePostForm
ヘルパー関数の導入:func parsePostForm(r *Request) (vs url.Values, err error)
という新しい内部ヘルパー関数が追加されました。- この関数は、リクエストの
Body
とContent-Type
ヘッダーを基に、POST/PUTリクエストのボディからフォームデータを解析する責任を負います。 application/x-www-form-urlencoded
形式のボディを処理し、最大10MBのボディサイズ制限を適用します。multipart/form-data
はParseMultipartForm
によって処理されるため、ここでは直接処理されません。
-
copyValues
ヘルパー関数の導入:func copyValues(dst, src url.Values)
というシンプルなヘルパー関数が追加されました。- これは、
src
のすべてのキーと値をdst
にコピーするために使用されます。
-
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ボディの値を優先的に返すようになります。
-
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つのファイルが変更されています。
src/pkg/net/http/request.go
:http.Request
構造体の定義と、フォーム解析に関連するメソッドが変更されました。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
がエラー時でもForm
とPostForm
を初期化することを確認します。
コアとなるコードの解説
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)
}
}
}
関連リンク
- Go Issue Tracker: https://github.com/golang/go/issues/3630
参考にした情報源リンク
- GitHub Commit: https://github.com/golang/go/commit/abb3c0618b658a41bf91a087f1737412e93ff6d9
- Go
net/http
issue 3630: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGIgCOV213gH8eIciaFggBlxJqGssOWYELgehbnvlNeH14q4_6wFOReW5mihcKHXiHjpZaxm8C3jFzrPzqel742doMjEsNupPTOooEuInxDAhKqAO3kAetiM_tE7tj267F39g== (Web検索結果より) - Go
net/http
package documentation (当時のバージョンに基づく): https://pkg.go.dev/net/http (現在のドキュメント) - Go
net/url
package documentation: https://pkg.go.dev/net/url