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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HTTP PATCH メソッドのリクエストボディが application/x-www-form-urlencoded 形式である場合に、Request.ParseForm メソッドがそのフォームデータを適切にパースできるようにする変更です。これにより、PATCH リクエストでも POSTPUT と同様にフォームデータにアクセスできるようになります。

コミット

commit b38320bffbc2239b5825566676004f4dc45d5444
Author: Matt Aimonetti <mattaimonetti@gmail.com>
Date:   Tue Mar 4 11:58:21 2014 -0800

    net/http: make Request.ParseForm parse form-urlencoded for method PATCH too
    
    Fixes #7454
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/70990044

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

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

元コミット内容

net/http: make Request.ParseForm parse form-urlencoded for method PATCH too Fixes #7454

このコミットは、net/http パッケージの Request.ParseForm メソッドが、PATCH メソッドのリクエストに対しても application/x-www-form-urlencoded 形式のフォームデータをパースするように変更するものです。これにより、PATCH リクエストのボディに含まれるフォームデータが Request.FormRequest.PostForm を通じてアクセス可能になります。

変更の背景

HTTP PATCH メソッドは、リソースの部分的な更新を適用するために使用されます。POSTPUT と異なり、PATCH はリソース全体を置き換えるのではなく、指定された変更のみを適用します。Webアプリケーションでは、フォームデータ(application/x-www-form-urlencodedmultipart/form-data)を使用してリクエストボディにデータを送信することが一般的です。

Goの net/http パッケージの Request.ParseForm メソッドは、以前は POST および PUT メソッドのリクエストボディに対してのみ application/x-www-form-urlencoded 形式のパースを行っていました。しかし、一部のアプリケーションやAPI設計では、PATCH メソッドでもフォームデータ形式で部分更新の情報を送信したいというニーズがありました。

このコミットは、Go issue #7454 で報告された問題に対応するものです。この問題は、PATCH リクエストで送信されたフォームデータが ParseForm によって処理されないため、開発者が PATCH リクエストのボディからフォームデータにアクセスできないというものでした。この変更により、PATCH メソッドの柔軟性が向上し、より幅広いWebアプリケーションのユースケースに対応できるようになります。

前提知識の解説

HTTPメソッド

HTTP (Hypertext Transfer Protocol) は、Web上でデータをやり取りするためのプロトコルです。HTTPメソッドは、クライアントがサーバーに対してどのような操作を行いたいかを伝えるために使用されます。主要なHTTPメソッドには以下のようなものがあります。

  • GET: リソースの取得。
  • POST: 新しいリソースの作成、または既存のリソースへのデータの送信。
  • PUT: リソースの完全な置き換え。
  • DELETE: リソースの削除。
  • PATCH: リソースの部分的な更新。

特に POSTPUTPATCH はリクエストボディを持つことができ、クライアントからサーバーへデータを送信するために利用されます。

application/x-www-form-urlencoded

これは、HTTPリクエストの Content-Type ヘッダで指定されるメディアタイプの一つで、Webフォームのデータをエンコードするための標準的な方法です。キーと値のペアが & で区切られ、各ペアは key=value の形式で表現されます。値はURLエンコードされます(例: スペースは %20 になる)。これは、HTMLフォームの method="POST"enctype="application/x-www-form-urlencoded" が指定された場合のデフォルトのエンコーディング形式です。

Go net/http パッケージ

net/http はGo言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。Webアプリケーションを構築する上で中心的な役割を果たします。

  • http.Request: クライアントからのHTTPリクエストを表す構造体です。リクエストメソッド、URL、ヘッダ、ボディなどの情報を含みます。
  • Request.ParseForm(): http.Request のメソッドで、リクエストのURLクエリパラメータと、POST または PUT (このコミット後は PATCH も含む) リクエストのボディからフォームデータをパースし、Request.Form および Request.PostForm フィールドに格納します。
    • Request.Form: URLクエリパラメータとリクエストボディのフォームデータを結合したものです。同じキーが存在する場合、ボディのデータが優先されます。
    • Request.PostForm: リクエストボディのフォームデータのみを含みます。
  • Request.FormValue(key string): Request.Form から指定されたキーの最初の値を取得する便利なメソッドです。ParseForm を自動的に呼び出します。
  • Request.PostFormValue(key string): Request.PostForm から指定されたキーの最初の値を取得する便利なメソッドです。ParseForm を自動的に呼び出します。

Goのテストフレームワーク

Goには組み込みのテストフレームワークがあり、go test コマンドで実行できます。テストファイルは通常、テスト対象のファイルと同じディレクトリに _test.go サフィックスを付けて配置されます。テスト関数は Test で始まり、*testing.T 型の引数を取ります。

技術的詳細

このコミットの技術的な核心は、net/http パッケージ内の Request.ParseForm メソッドのロジック変更にあります。

Request.ParseForm メソッドは、HTTPリクエストのボディをパースしてフォームデータを抽出する役割を担っています。以前の実装では、このパース処理は r.Method == "POST" || r.Method == "PUT" という条件に基づいていました。これは、伝統的にフォームデータが POSTPUT リクエストで送信されることが多かったためです。

しかし、RESTful APIの設計や、よりセマンティックなHTTPメソッドの利用が広まるにつれて、リソースの部分更新を意味する PATCH メソッドでもフォームデータ形式で情報を送信したいというニーズが出てきました。例えば、特定のフィールドだけを更新する際に、application/x-www-form-urlencoded 形式で field=newValue のように送りたい場合などです。

このコミットでは、この条件式に || r.Method == "PATCH" を追加することで、PATCH メソッドのリクエストも POSTPUT と同様に parsePostForm(r) 関数によってボディのパース対象としました。parsePostForm 関数は、リクエストの Content-Type ヘッダが application/x-www-form-urlencoded または multipart/form-data である場合に、適切にボディを読み込み、キーと値のペアを抽出して url.Values 型のマップとして返します。

この変更により、PATCH リクエストのボディが application/x-www-form-urlencoded 形式であれば、Request.ParseForm() を呼び出すことで Request.PostForm にそのデータが格納され、Request.Form にもクエリパラメータと結合された形でデータが利用可能になります。これにより、開発者は Request.FormValue()Request.PostFormValue() といった便利なメソッドを使って、PATCH リクエストのボディから直接フォームデータにアクセスできるようになります。

また、この変更は後方互換性を維持しつつ、net/http パッケージの柔軟性とHTTP標準への準拠を向上させるものです。既存の POSTPUT リクエストの処理には影響を与えません。

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

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/net/http/request.go: Request.ParseForm メソッドのロジックが変更されています。
  2. src/pkg/net/http/request_test.go: PATCH メソッドのフォームデータパースを検証するための新しいテストケースが追加されています。
diff --git a/src/pkg/net/http/request.go b/src/pkg/net/http/request.go
index 7a97770314..480baf3aee 100644
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -728,7 +728,7 @@ func parsePostForm(r *Request) (vs url.Values, err error) {
 func (r *Request) ParseForm() error {
  	var err error
  	if r.PostForm == nil {
- 		if r.Method == "POST" || r.Method == "PUT" {
+ 		if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
  			r.PostForm, err = parsePostForm(r)
  		}
  		if r.PostForm == nil {
diff --git a/src/pkg/net/http/request_test.go b/src/pkg/net/http/request_test.go
index 68d141398a..223e02c294 100644
--- a/src/pkg/net/http/request_test.go
+++ b/src/pkg/net/http/request_test.go
@@ -60,6 +60,37 @@ func TestPostQuery(t *testing.T) {
  	}\n }\n \n+func TestPatchQuery(t *testing.T) {
+\treq, _ := NewRequest("PATCH", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&empty=not",
+\t\tstrings.NewReader("z=post&both=y&prio=2&empty="))
+\treq.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
+\n+\tif q := req.FormValue("q"); q != "foo" {
+\t\tt.Errorf(`req.FormValue("q") = %q, want "foo"`, q)
+\t}
+\tif z := req.FormValue("z"); z != "post" {
+\t\tt.Errorf(`req.FormValue("z") = %q, want "post"`, z)
+\t}
+\tif bq, found := req.PostForm["q"]; found {
+\t\tt.Errorf(`req.PostForm["q"] = %q, want no entry in map`, bq)
+\t}
+\tif bz := req.PostFormValue("z"); bz != "post" {
+\t\tt.Errorf(`req.PostFormValue("z") = %q, want "post"`, bz)
+\t}
+\tif qs := req.Form["q"]; !reflect.DeepEqual(qs, []string{"foo", "bar"}) {
+\t\tt.Errorf(`req.Form["q"] = %q, want ["foo", "bar"]`, qs)
+\t}
+\tif both := req.Form["both"]; !reflect.DeepEqual(both, []string{"y", "x"}) {
+\t\tt.Errorf(`req.Form["both"] = %q, want ["y", "x"]`, both)
+\t}
+\tif prio := req.FormValue("prio"); prio != "2" {
+\t\tt.Errorf(`req.FormValue("prio") = %q, want "2" (from body)`, prio)
+\t}
+\tif empty := req.FormValue("empty"); empty != "" {
+\t\tt.Errorf(`req.FormValue("empty") = %q, want "" (from body)`, empty)
+\t}
+}\n+\n type stringMap map[string][]string
 type parseContentTypeTest struct {
  	shouldError bool

コアとなるコードの解説

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

Request.ParseForm メソッド内の以下の行が変更されています。

- 		if r.Method == "POST" || r.Method == "PUT" {
+ 		if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {

この変更は非常にシンプルですが、その影響は大きいです。 r.PostForm == nil の条件は、ParseForm がまだ実行されていないことを確認しています。その内部で、リクエストメソッドが POST または PUT の場合にのみ parsePostForm(r) が呼び出され、リクエストボディのフォームデータがパースされて r.PostForm に格納されていました。

このコミットにより、PATCH メソッドもこの条件に追加され、PATCH リクエストのボディも parsePostForm によって処理されるようになりました。これにより、PATCH リクエストで application/x-www-form-urlencoded 形式のデータが送信された場合でも、Request.PostForm および Request.Form を通じてそのデータにアクセスできるようになります。

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

TestPatchQuery という新しいテスト関数が追加されています。このテストは、PATCH メソッドのリクエストに対して Request.ParseForm が正しく動作するかを検証します。

テストケースの概要:

  1. NewRequest を使用して PATCH メソッドのHTTPリクエストを作成します。
    • URLクエリパラメータ (q=foo&q=bar&both=x&prio=1&empty=not) と、
    • application/x-www-form-urlencoded 形式のリクエストボディ (z=post&both=y&prio=2&empty=) を含みます。
  2. Content-Type ヘッダを application/x-www-form-urlencoded に設定します。
  3. req.FormValuereq.PostFormreq.Form を使用して、パースされたフォームデータが期待通りの値を持っているかを確認します。

特に重要な検証ポイントは以下の通りです。

  • req.FormValue("q"): URLクエリパラメータから foo が取得できること。
  • req.FormValue("z"): リクエストボディから post が取得できること。
  • req.PostForm["q"]: PostForm にはクエリパラメータの q が含まれないこと(PostForm はボディのみを対象とするため)。
  • req.PostFormValue("z"): PostFormValue でボディの z が取得できること。
  • req.Form["q"]: Form にはURLクエリパラメータの q の両方の値 (foo, bar) が含まれること。
  • req.Form["both"]: Form ではボディの both (y) がクエリパラメータの both (x) よりも優先されること (y, x の順で取得される)。
  • req.FormValue("prio"): FormValue はボディの prio (2) を優先すること。
  • req.FormValue("empty"): ボディの空の値 (empty=) が正しくパースされ、空文字列として取得できること。

これらのテストは、PATCH メソッドのリクエストボディが ParseForm によって正しく処理され、URLクエリパラメータとボディのフォームデータが期待通りに結合・優先されることを保証します。

関連リンク

参考にした情報源リンク

  • Go issue #7454 の議論内容
  • Go net/http パッケージのソースコード
  • HTTP/1.1 Semantics and Content (RFC 7231)
  • The PATCH Method for HTTP (RFC 5789)
  • MDN Web Docs: HTTP methods
  • MDN Web Docs: Content-Type
  • Go言語の公式ドキュメントとブログ記事 (一般的な net/http の利用方法について)
  • Stack Overflow や技術ブログ (GoのHTTPサーバーとフォームデータ処理に関する一般的な情報)