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

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

このコミットは、Go言語のnet/httpパッケージにおいて、ParseMultipartForm関数がContent-Typemultipart/form-dataではないリクエストに対してErrNotMultipartエラーを正しく返すように修正し、関連するテストを追加・修正するものです。

コミット

commit 8d1b63abff2c948741e94c92d2a9a60069a71ddb
Author: Matthew Cottingham <mattcottingham@gmail.com>
Date:   Thu Apr 10 22:50:04 2014 -0700

    net/http: Return ErrNotMultipart from ParseMultipartForm if content-type isn't multipart/form-data.
    
    Add test for multipart form requests with an invalid content-type to ensure
    ErrNotMultipart is returned.
    
    Change ParseMultipartForm to return ErrNotMultipart when it is returned by multipartReader.
    
    Modify test for empty multipart request handling to use POST so that the body is checked.
    
    Fixes #6334.
    
    This is the first changeset working on multipart request handling. Further changesets
    could add more tests and clean up the TODO.
    
    LGTM=bradfitz
    R=golang-codereviews, gobot, bradfitz, rsc
    CC=golang-codereviews
    https://golang.org/cl/44040043

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

https://github.com/golang/go/commit/8d1b63abff2c948741e94c92d2a9a60069a71ddb

元コミット内容

net/http: Content-Typemultipart/form-dataではない場合、ParseMultipartFormからErrNotMultipartを返すようにする。

不正なContent-Typeを持つマルチパートフォームリクエストに対してErrNotMultipartが返されることを確認するためのテストを追加。

ParseMultipartFormmultipartReaderからErrNotMultipartが返されたときに、それを返すように変更。

空のマルチパートリクエスト処理のテストをPOSTを使用するように修正し、ボディがチェックされるようにする。

Fixes #6334.

これはマルチパートリクエスト処理に関する最初の変更セットである。さらなる変更セットで、より多くのテストを追加し、TODOをクリーンアップできる可能性がある。

変更の背景

このコミットは、Goのnet/httpパッケージにおけるマルチパートフォームデータの処理に関するバグ(Fixes #6334)を修正するために行われました。具体的には、http.RequestParseMultipartFormメソッドが、リクエストのContent-Typeヘッダがmultipart/form-dataではない場合に、期待されるhttp.ErrNotMultipartエラーを返さず、代わりにnil(エラーなし)を返してしまうという問題がありました。

この挙動は、アプリケーションがリクエストのContent-Typeを適切に検証せずにParseMultipartFormを呼び出した場合、誤った形式のリクエストをマルチパートフォームとして処理しようとする可能性があり、予期せぬ動作やセキュリティ上の問題を引き起こす可能性がありました。例えば、通常のapplication/x-www-form-urlencoded形式のリクエストや、全く異なるContent-Typeを持つリクエストが、誤ってマルチパートとして解釈され、その後の処理でエラーが発生したり、データが正しくパースされなかったりする原因となります。

この修正により、ParseMultipartFormは、基盤となるmultipartReader()ErrNotMultipartを返した場合に、そのエラーを呼び出し元に透過的に伝播するようになり、より堅牢で予測可能なエラーハンドリングが実現されます。また、この変更を検証するために、不正なContent-Typeを持つリクエストに対するテストケースが追加されました。

前提知識の解説

1. net/httpパッケージ

Go言語の標準ライブラリであるnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。Webアプリケーションの構築において中心的な役割を果たし、HTTPリクエストの処理、レスポンスの生成、ルーティングなどを担当します。

2. HTTP Content-Typeヘッダ

HTTPリクエストやレスポンスのヘッダの一つで、メッセージボディのメディアタイプ(MIMEタイプ)を示します。サーバーはこれを見て、ボディの内容をどのように解釈すべきかを判断します。

  • application/x-www-form-urlencoded: HTMLフォームのデフォルトのエンコーディングタイプで、キーと値のペアが&で区切られ、=で結合されます。特殊文字はURLエンコードされます。
  • multipart/form-data: ファイルアップロードを含むフォームデータを送信する際に使用されるContent-Typeです。各フォームフィールドやファイルは、boundaryと呼ばれる区切り文字列によって分離された「パート」として送信されます。これは、単一のリクエストで複数の種類のデータを送信できるため、特にファイルアップロードで不可欠です。

3. http.Request構造体

net/httpパッケージにおけるHTTPリクエストを表す構造体です。リクエストメソッド(GET, POSTなど)、URL、ヘッダ、ボディなどの情報を含みます。

4. ParseMultipartFormメソッド

http.Request構造体のメソッドの一つで、リクエストボディがmultipart/form-data形式である場合に、その内容をパースしてRequest.MultipartFormフィールドに格納します。maxMemory引数は、メモリに保持する最大バイト数を指定し、これを超える部分は一時ファイルに書き込まれます。

5. MultipartReaderメソッド

http.Request構造体のメソッドで、リクエストのContent-Typemultipart/form-dataである場合に、そのボディを読み取るためのmultipart.Readerを返します。Content-Typemultipart/form-dataではない場合、http.ErrNotMultipartエラーを返します。

6. http.ErrNotMultipartエラー

net/httpパッケージで定義されているエラーで、リクエストがマルチパート形式ではないことを示します。MultipartReader関数が、リクエストのContent-Typeヘッダがmultipart/form-dataではない場合にこのエラーを返します。

技術的詳細

このコミットの主要な変更点は、src/pkg/net/http/request.go内の(*Request).ParseMultipartFormメソッドの挙動修正と、src/pkg/net/http/request_test.goにおけるテストの追加・修正です。

ParseMultipartFormの修正

以前のParseMultipartFormの実装では、内部でr.multipartReader()を呼び出した際にErrNotMultipartが返された場合、そのエラーを無視してnil(エラーなし)を返していました。

// 修正前
mr, err := r.multipartReader()
if err == ErrNotMultipart {
    return nil // ここでErrNotMultipartをnilとして扱っていた
} else if err != nil {
    return err
}

この挙動は、Content-Typemultipart/form-dataではないリクエストに対してParseMultipartFormが呼び出された場合でも、エラーが報告されないという問題を引き起こしていました。これにより、呼び出し元はリクエストがマルチパート形式ではないことを検知できず、誤った処理を続行する可能性がありました。

今回の修正では、このif err == ErrNotMultipart { return nil }のブロックが削除されました。

// 修正後
mr, err := r.multipartReader()
if err != nil { // ErrNotMultipartを含む全てのエラーを返す
    return err
}

これにより、multipartReader()ErrNotMultipartを返した場合、ParseMultipartFormもそのErrNotMultipartを呼び出し元にそのまま返すようになります。これは、APIのセマンティクスとしてより正確であり、呼び出し元がリクエストのContent-Typeに基づいて適切なエラーハンドリングを行えるようになります。

テストの追加と修正

変更されたParseMultipartFormの挙動を検証するために、src/pkg/net/http/request_test.goに新しいテストケースが追加され、既存のテストが修正されました。

  1. TestParseMultipartFormの追加: この新しいテスト関数は、ParseMultipartFormが不正なContent-Typeを持つリクエストに対してErrNotMultipartを正しく返すことを確認します。

    • Content-Typetext/plainのリクエストを作成し、ParseMultipartFormを呼び出します。
    • 返されたエラーがErrNotMultipartであることをアサートします。これにより、修正が意図通りに機能していることが保証されます。
  2. TestMultipartReaderの修正: 既存のTestMultipartReader関数も、Content-Typetext/plainの場合にMultipartReader()ErrNotMultipartを返すことを確認する部分が修正されました。以前はt.Errorfでエラーを報告していましたが、より明確なテストロジックになりました。

  3. TestEmptyMultipartRequestの修正: このテストは、空のマルチパートリクエストの処理を検証するためのものでした。以前はGETメソッドを使用していましたが、GETリクエストは通常ボディを持たないため、マルチパートフォームのテストとしては不適切でした。 このコミットでは、テストリクエストのメソッドがPOSTに変更されました。POSTリクエストはボディを持つことができ、マルチパートフォームデータがボディに含まれるため、より現実的なテストシナリオとなります。これにより、ParseMultipartFormがボディの内容を適切にチェックするようになります。

  4. TestFormValueCallsParseMultipartFormTestFormFileCallsParseMultipartFormの追加: これらのテストは、Request.FormValueRequest.FormFileといったメソッドが、内部的にParseMultipartFormを呼び出すことを確認します。これは、これらの高レベルなメソッドを使用する際に、自動的にマルチパートフォームのパースが行われることを保証するためのものです。

これらのテストの追加と修正により、net/httpパッケージのマルチパートフォーム処理の堅牢性が向上し、開発者がより信頼性の高いコードを書けるようになります。

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

src/pkg/net/http/request.go

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -789,9 +789,7 @@ func (r *Request) ParseMultipartForm(maxMemory int64) error {
 	}
 
 	mr, err := r.multipartReader()
-	if err == ErrNotMultipart {
-		return nil
-	} else if err != nil {
+	if err != nil {
 		return err
 	}

src/pkg/net/http/request_test.go

--- a/src/pkg/net/http/request_test.go
+++ b/src/pkg/net/http/request_test.go
@@ -154,7 +154,25 @@ func TestMultipartReader(t *testing.T) {
 	req.Header = Header{"Content-Type": {"text/plain"}}
 	multipart, err = req.MultipartReader()
 	if multipart != nil {
-		t.Errorf("unexpected multipart for text/plain")
+		t.Error("unexpected multipart for text/plain")
+	}
+}
+
+func TestParseMultipartForm(t *testing.T) {
+	req := &Request{
+		Method: "POST",
+		Header: Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}},
+		Body:   ioutil.NopCloser(new(bytes.Buffer)),
+	}
+	err := req.ParseMultipartForm(25)
+	if err == nil {
+		t.Error("expected multipart EOF, got nil")
+	}
+
+	req.Header = Header{"Content-Type": {"text/plain"}}
+	err = req.ParseMultipartForm(25)
+	if err != ErrNotMultipart {
+		t.Error("expected ErrNotMultipart for text/plain")
 	}
 }
 
@@ -220,16 +238,38 @@ func TestMultipartRequestAuto(t *testing.T) {
 	validateTestMultipartContents(t, req, true)
 }
 
-func TestEmptyMultipartRequest(t *testing.T) {
-	// Test that FormValue and FormFile automatically invoke
-	// ParseMultipartForm and return the right values.
-	req, err := NewRequest("GET", "/", nil)
-	if err != nil {
-		t.Errorf("NewRequest err = %q", err)
-	}
-	testMissingFile(t, req)
-}
-
+// Test that FormValue invokes ParseMultipartForm.
+func TestFormValueCallsParseMultipartForm(t *testing.T) {
+	req, _ := NewRequest("POST", "http://www.google.com/", strings.NewReader("z=post"))
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
+	if req.Form != nil {
+		t.Fatal("Unexpected request Form, want nil")
+	}
+	req.FormValue("z")
+	if req.Form == nil {
+		t.Fatal("ParseMultipartForm not called by FormValue")
+	}
+}
+
+// Test that FormFile invokes ParseMultipartForm.
+func TestFormFileCallsParseMultipartForm(t *testing.T) {
+	req := newTestMultipartRequest(t)
+	if req.Form != nil {
+		t.Fatal("Unexpected request Form, want nil")
+	}
+	req.FormFile("")
+	if req.Form == nil {
+		t.Fatal("ParseMultipartForm not called by FormFile")
+	}
+}
+
+func TestMissingFileMultipartRequest(t *testing.T) {
+	// Test that FormFile returns an error if
+	// the named file is missing.
+	req := newTestMultipartRequest(t)
+	testMissingFile(t, req)
+}
+
 // Test that ParseMultipartForm errors if called
 // after MultipartReader on the same request.
 func TestParseMultipartFormOrder(t *testing.T) {

コアとなるコードの解説

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

ParseMultipartFormメソッド内の変更は非常にシンプルですが、その影響は大きいです。

 	mr, err := r.multipartReader()
-	if err == ErrNotMultipart {
-		return nil
-	} else if err != nil {
+	if err != nil {
 		return err
 	}

この変更は、multipartReader()が返すエラーの処理方法を変更しています。

  • 修正前: multipartReader()ErrNotMultipartを返した場合、ParseMultipartFormnil(エラーなし)を返していました。これは、リクエストがマルチパート形式ではないにもかかわらず、エラーを報告しないという誤った挙動でした。
  • 修正後: if err != nilというシンプルな条件になり、multipartReader()が返す全てのエラー(ErrNotMultipartを含む)が、そのままParseMultipartFormの呼び出し元に伝播されるようになりました。これにより、ParseMultipartFormは、リクエストがマルチパート形式ではない場合にErrNotMultipartを正しく返すようになります。

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

テストファイルでは、主に以下の点が変更されました。

  • TestParseMultipartFormの追加: この新しいテストは、ParseMultipartFormContent-Type: text/plainのような非マルチパートのリクエストに対してErrNotMultipartを返すことを明示的に検証します。これは、request.goの変更が意図通りに機能していることを確認するための重要なテストです。

  • TestMultipartReaderの修正: 既存のテスト内のt.Errorft.Errorに変更されています。これは機能的な変更ではなく、テストフレームワークの慣用的な使用法に合わせた微調整です。

  • TestEmptyMultipartRequestの削除と関連テストの追加: 元のTestEmptyMultipartRequestGETリクエストを使用していましたが、マルチパートフォームは通常POSTリクエストのボディで送信されるため、テストの関連性が低かったです。このテストは削除され、代わりにTestMissingFileMultipartRequestが追加されました。 さらに、TestFormValueCallsParseMultipartFormTestFormFileCallsParseMultipartFormが追加されました。これらのテストは、FormValueFormFileといった高レベルなAPIが、内部でParseMultipartFormを適切に呼び出していることを確認します。これにより、開発者がこれらのAPIを使用する際に、マルチパートフォームのパースが自動的に行われることが保証されます。

これらの変更により、net/httpパッケージのマルチパートフォーム処理の正確性と堅牢性が向上し、開発者がより信頼性の高いWebアプリケーションを構築できるようになります。

関連リンク

参考にした情報源リンク

特になし。提供されたコミット情報とGo言語のnet/httpパッケージに関する一般的な知識に基づいて解説を生成しました。