[インデックス 12217] ファイルの概要
このコミットでは、Go言語の標準ライブラリ net/http/cgi
パッケージにおけるテストが追加・修正されています。具体的には、CGIハンドラがヘッダーのみを書き込み、ボディを全く書き込まない場合の挙動を検証するための新しいテストケースが導入されています。
変更されたファイルは以下の通りです。
src/pkg/net/http/cgi/host_test.go
: 1行追加src/pkg/net/http/cgi/matryoshka_test.go
: 21行追加、1行削除
コミット
- コミットハッシュ:
761f946617eb12630095954b436ab11e2cd1f05f
- 作者: Brad Fitzpatrick bradfitz@golang.org
- コミット日時: Sun Feb 26 14:46:22 2012 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/761f946617eb12630095954b436ab11e2cd1f05f
元コミット内容
net/http/cgi: add an empty response test
New test for http://code.google.com/p/go/source/detail?r=a73ba18
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5701046
変更の背景
このコミットの背景には、Go言語の net/http/cgi
パッケージが、CGIスクリプトがHTTPレスポンスボディを全く返さずにヘッダーのみを返すようなケースを適切に処理できるかどうかの検証が必要になったという経緯があります。
コミットメッセージに記載されている http://code.google.com/p/go/source/detail?r=a73ba18
は、Goプロジェクトの以前のコードレビューシステムであるGoogle Codeの変更セットへのリンクです。このリンク先の変更は、CGIハンドラが空のレスポンスボディを返す場合の挙動に関連するものであったと推測されます。具体的には、CGIスクリプトが Content-Length: 0
ヘッダーを送信するか、あるいは単にボディを何も書き込まない場合に、GoのCGIハンドラがこれを正しく解釈し、クライアントに適切なHTTPレスポンスを返すことを保証するためのテストが不足していたと考えられます。
このようなケースは、例えばCGIスクリプトがリダイレクト(Location
ヘッダーのみ)を行う場合や、単にステータスコードとヘッダーのみを返して処理を終了する場合などに発生します。Goの net/http/cgi
パッケージがこれらのエッジケースを堅牢に扱えることを確認するために、この新しいテストが追加されました。
前提知識の解説
CGI (Common Gateway Interface)
CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携するための標準的なプロトコルです。Webサーバーはクライアントからのリクエストを受け取ると、CGIスクリプトを起動し、環境変数や標準入力(POSTデータなど)を通じてリクエスト情報をスクリプトに渡します。CGIスクリプトは処理を実行し、結果を標準出力にHTTPヘッダーとボディの形式で出力します。Webサーバーはその出力を受け取り、クライアントに返します。
CGIはシンプルで汎用性が高い一方で、リクエストごとにプロセスを起動するため、パフォーマンス上のオーバーヘッドが大きいという欠点があります。しかし、そのシンプルさから、様々な言語でWebアプリケーションを開発する際の基本的なインターフェースとして広く利用されてきました。
Go言語の net/http/cgi
パッケージ
Go言語の標準ライブラリ net/http/cgi
パッケージは、GoプログラムをCGIスクリプトとして実行したり、GoのHTTPハンドラをCGI環境でホストしたりするための機能を提供します。
cgi.Serve(handler http.Handler)
: この関数は、現在のGoプログラムをCGIスクリプトとして実行し、指定されたhttp.Handler
をCGIリクエストの処理に利用します。WebサーバーがGoプログラムをCGIとして起動すると、この関数がCGIプロトコルに従ってリクエストを解析し、handler
に渡します。handler
からのレスポンスはCGIプロトコルに従って標準出力に書き込まれます。cgi.Handler
構造体: この構造体は、GoのHTTPハンドラをCGIスクリプトとして実行するための設定をカプセル化します。Path
フィールドでCGIスクリプトの実行可能ファイルのパスを指定し、Root
やArgs
などでCGI環境をカスタマイズできます。これにより、GoのHTTPサーバー内で別のGoプログラム(または他のCGIスクリプト)をCGIとして起動し、そのレスポンスを処理するといった「CGI-in-CGI」のようなシナリオも実現できます。
Go言語のテストフレームワーク
Go言語には、標準で強力なテストフレームワークが組み込まれています。
testing
パッケージ: Goのテストはtesting
パッケージを利用して記述されます。テストファイルは通常、テスト対象のソースファイルと同じディレクトリに_test.go
というサフィックスを付けて配置されます。- テスト関数の命名規則: テスト関数は
TestXxx
という形式で命名され、*testing.T
型の引数を一つ取ります。 t.Errorf(...)
/t.Fatalf(...)
: テスト中にエラーを報告するために使用されます。t.Errorf
はテストを失敗としてマークしますが、テストの実行は続行します。t.Fatalf
はテストを失敗としてマークし、直ちにテストの実行を停止します。httptest
パッケージ: HTTPハンドラのテストを容易にするためのユーティリティを提供します。httptest.NewRecorder()
はhttp.ResponseWriter
の実装を提供し、ハンドラが書き込んだレスポンスヘッダーやボディをメモリ上にキャプチャできます。これにより、実際のHTTPサーバーを起動することなく、ハンドラの挙動を検証できます。
技術的詳細
このコミットで追加されたテストは、net/http/cgi
パッケージがCGIスクリプトからの「ボディなし」レスポンスを正しく処理できることを保証することを目的としています。
具体的なシナリオは以下の通りです。
-
親CGIハンドラ (
TestChildOnlyHeaders
):net/http/cgi.Handler
を設定し、自身(テストバイナリ)をCGIスクリプトとして起動します。- 起動する子CGIプロセスには、
TestBeChildCGIProcess
というテスト関数を実行させるように引数を渡します。 - さらに、子CGIプロセスに対して
no-body=1
というクエリパラメータを含むHTTPリクエストを送信します。 runCgiTest
ヘルパー関数を使用して、このCGIリクエストを実行し、子CGIプロセスからのレスポンスをキャプチャします。- 期待される結果は、レスポンスボディが空であること (
_body
が空文字列) と、子CGIが設定した特定のヘッダー (X-Test-Header
) が存在することです。
-
子CGIプロセス (
TestBeChildCGIProcess
):- この関数は、親CGIハンドラによってCGIスクリプトとして起動されます。
os.Getenv("REQUEST_METHOD")
をチェックして、自身がCGI環境で実行されていることを確認します。cgi.Serve
を呼び出し、http.HandlerFunc
をCGIリクエストの処理に利用します。- この
http.HandlerFunc
の内部では、まずX-Test-Header
をレスポンスヘッダーに設定します。 - 次に、リクエストのフォーム値をパースし、
no-body=1
というクエリパラメータが存在するかどうかをチェックします。 - もし
no-body=1
が存在する場合、レスポンスボディを何も書き込まずに関数をreturn
します。これが「空のレスポンスボディ」のシナリオをシミュレートする重要な部分です。 no-body=1
が存在しない場合は、通常通りfmt.Fprintf
を使ってレスポンスボディを書き込みます。
このテストの目的は、子CGIプロセスがヘッダーのみを送信し、ボディを送信しない場合に、親CGIハンドラがそのレスポンスを正しく処理し、ボディが空であることを確認することです。これにより、CGIプロトコルのエッジケースに対する net/http/cgi
パッケージの堅牢性が向上します。
コアとなるコードの変更箇所
src/pkg/net/http/cgi/host_test.go
--- a/src/pkg/net/http/cgi/host_test.go
+++ b/src/pkg/net/http/cgi/host_test.go
@@ -41,6 +41,7 @@ func runCgiTest(t *testing.T, h *Handler, httpreq string, expectedMap map[string
// Make a map to hold the test map that the CGI returns.
m := make(map[string]string)
+ m["_body"] = rw.Body.String()
linesRead := 0
readlines:
for {
src/pkg/net/http/cgi/matryoshka_test.go
--- a/src/pkg/net/http/cgi/matryoshka_test.go
+++ b/src/pkg/net/http/cgi/matryoshka_test.go
@@ -51,6 +51,22 @@ func TestHostingOurselves(t *testing.T) {
}\n}\n\n+// Test that a child handler only writing headers works.\n+func TestChildOnlyHeaders(t *testing.T) {\n+\th := &Handler{\n+\t\tPath: os.Args[0],\n+\t\tRoot: "/test.go",\n+\t\tArgs: []string{"-test.run=TestBeChildCGIProcess"},\n+\t}\n+\texpectedMap := map[string]string{\n+\t\t"_body": "",\n+\t}\n+\treplay := runCgiTest(t, h, "GET /test.go?no-body=1 HTTP/1.0\\nHost: example.com\\n\\n", expectedMap)\n+\tif expected, got := "X-Test-Value", replay.Header().Get("X-Test-Header"); got != expected {\n+\t\tt.Errorf("got a X-Test-Header of %q; expected %q", got, expected)\n+\t}\n+}\n+\n // Note: not actually a test.\n func TestBeChildCGIProcess(t *testing.T) {\n \tif os.Getenv("REQUEST_METHOD") == "" {\n@@ -59,8 +75,11 @@ func TestBeChildCGIProcess(t *testing.T) {\n \t}\n \tServe(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n \t\trw.Header().Set("X-Test-Header", "X-Test-Value")\n-\t\tfmt.Fprintf(rw, "test=Hello CGI-in-CGI\\n")\n \t\treq.ParseForm()\n+\t\tif req.FormValue("no-body") == "1" {\n+\t\t\treturn\n+\t\t}\n+\t\tfmt.Fprintf(rw, "test=Hello CGI-in-CGI\\n")\n \t\tfor k, vv := range req.Form {\n \t\t\tfor _, v := range vv {\n \t\t\t\tfmt.Fprintf(rw, "param-%s=%s\\n", k, v)\n```
## コアとなるコードの解説
### `src/pkg/net/http/cgi/host_test.go` の変更
* `runCgiTest` 関数は、CGIテストを実行するためのヘルパー関数です。
* 追加された行 `m["_body"] = rw.Body.String()` は、`httptest.ResponseRecorder` (`rw`) にキャプチャされたレスポンスボディの内容を、結果を格納するマップ `m` に `_body` というキーで保存するようにしています。
* これにより、テスト関数内でCGIレスポンスのボディが期待通りに空であるか、あるいは特定の内容を含んでいるかを簡単に検証できるようになります。
### `src/pkg/net/http/cgi/matryoshka_test.go` の変更
1. **`TestChildOnlyHeaders` 関数の追加**:
* この新しいテスト関数は、CGIハンドラがヘッダーのみを書き込み、ボディを書き込まないケースをテストします。
* `Handler` 構造体を初期化し、`Path` には現在のテストバイナリ自身 (`os.Args[0]`) を指定します。これは、Goのテストバイナリがサブプロセスとして自身をCGIスクリプトとして実行できる「マトリョーシカ人形」のようなテスト設定です。
* `Args` には `-test.run=TestBeChildCGIProcess` を設定し、子プロセスで `TestBeChildCGIProcess` 関数が実行されるようにします。
* `expectedMap` には `"_body": ""` を設定し、レスポンスボディが空であることを期待します。
* `runCgiTest` を呼び出し、`GET /test.go?no-body=1` というHTTPリクエストを送信します。`no-body=1` は、子CGIプロセスがボディを書き込まないようにするためのフラグです。
* 最後に、`replay.Header().Get("X-Test-Header")` をチェックし、子CGIが設定した `X-Test-Header` が期待通り `X-Test-Value` であることを検証します。
2. **`TestBeChildCGIProcess` 関数の変更**:
* この関数は、`TestChildOnlyHeaders` によってCGIプロセスとして起動されます。
* `Serve` 関数に渡される `http.HandlerFunc` の内部で変更が行われています。
* `req.ParseForm()` の後、`if req.FormValue("no-body") == "1"` という条件が追加されました。
* もし `no-body=1` クエリパラメータが存在する場合、`return` ステートメントが実行され、それ以降の `fmt.Fprintf` によるボディの書き込みがスキップされます。これにより、CGIレスポンスがヘッダーのみで構成され、ボディが空になるシナリオが実現されます。
* この変更により、`TestChildOnlyHeaders` が意図する「ボディなし」のCGIレスポンスを生成できるようになり、`net/http/cgi` パッケージがそのケースを正しく処理できるかどうかの検証が可能になります。
これらの変更は、`net/http/cgi` パッケージの堅牢性を高め、CGIプロトコルの様々な有効なレスポンス形式(特にボディが空の場合)に対する互換性を保証するために重要です。
## 関連リンク
* GitHubでのコミットページ: [https://github.com/golang/go/commit/761f946617eb12630095954b436ab11e2cd1f05f](https://github.com/golang/go/commit/761f946617eb12630095954b436ab11e2cd1f05f)
* Go Gerrit Code Review (CL): [https://golang.org/cl/5701046](https://golang.org/cl/5701046)
## 参考にした情報源リンク
* Go Issue Tracker (関連する可能性のある変更セット): [http://code.google.com/p/go/source/detail?r=a73ba18](http://code.google.com/p/go/source/detail?r=a73ba18)
* Go Programming Language Documentation: `net/http/cgi` package: [https://pkg.go.dev/net/http/cgi](https://pkg.go.dev/net/http/cgi)
* Go Programming Language Documentation: `testing` package: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go Programming Language Documentation: `net/http/httptest` package: [https://pkg.go.dev/net/http/httptest](https://pkg.go.dev/net/http/httptest)
* Common Gateway Interface (CGI) - Wikipedia: [https://ja.wikipedia.org/wiki/Common_Gateway_Interface](https://ja.wikipedia.org/wiki/Common_Gateway_Interface)