[インデックス 14626] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおいて、HTTP POSTリクエスト後のリダイレクト処理を改善するものです。具体的には、特定のHTTPステータスコード(302 Found および 303 See Other)を受け取った際に、POSTリクエストが自動的にリダイレクトされるように変更し、その際のリダイレクト先のメソッドをGETに変更する挙動を導入しています。これにより、Webアプリケーションにおける一般的な「Post/Redirect/Get」パターンへの対応が強化されます。
コミット
commit 08ce7f1d5c309cf4187fbb1e442ee4388c29212b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Dec 12 11:09:55 2012 -0800
net/http: follow certain redirects after POST requests
Fixes #4145
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6923055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/08ce7f1d5c309cf4187fbb1e442ee4388c29212b
元コミット内容
このコミットは、Goの net/http
パッケージにおけるHTTPクライアントのリダイレクト処理を修正するものです。特に、POSTリクエストがリダイレクトされた際の挙動に焦点を当てています。
変更前は、http.Client
はGETまたはHEADリクエストに対してのみ自動的にリダイレクトを追跡していました。POSTリクエストがリダイレクトレスポンス(例: 301, 302, 303)を受け取った場合、クライアントは自動的にリダイレクトを追跡せず、リダイレクトレスポンスをそのまま返していました。
このコミットでは、POSTリクエストが302 (Found) または 303 (See Other) のステータスコードを受け取った場合に、自動的にリダイレクトを追跡するように変更されます。さらに重要な点として、これらのリダイレクトでは、リダイレクト先のURLへのリクエストメソッドがPOSTからGETに変更されます。これは、HTTP/1.1の仕様で推奨される「Post/Redirect/Get」パターンに準拠するための変更です。
また、この変更はGoのIssue #4145を修正するものです。
変更の背景
この変更の背景には、HTTPプロトコルのリダイレクトに関する一般的な慣習と、Goの net/http
クライアントの既存の挙動との乖離がありました。
- HTTPリダイレクトの複雑性: HTTPリダイレクトは、ステータスコード(301 Moved Permanently, 302 Found, 303 See Other, 307 Temporary Redirect, 308 Permanent Redirectなど)によって異なる意味と挙動を持ちます。特に、POSTリクエストがリダイレクトされる場合、その後のリクエストが元のメソッド(POST)を維持すべきか、それともGETに変更すべきかという点で、歴史的にブラウザやクライアントの実装間で一貫性がない時期がありました。
- Post/Redirect/Get (PRG) パターン: Webアプリケーション開発では、フォームの送信(POSTリクエスト)後にユーザーを別のページにリダイレクトする際に、PRGパターンが広く採用されています。これは、ユーザーがブラウザの「戻る」ボタンを押した際にフォームの再送信を防ぎ、またページのリロードによって意図しない二重送信が発生するのを防ぐためのベストプラクティスです。PRGパターンでは、POSTリクエストの後に302または303リダイレクトを返し、クライアントはリダイレクト先のURLにGETリクエストを発行します。
- Goクライアントの既存の挙動: このコミット以前のGoの
net/http
クライアントは、POSTリクエストに対するリダイレクトを自動的に追跡しませんでした。これは、PRGパターンを実装する際に開発者が手動でリダイレクトを処理する必要があることを意味し、利便性が低いという問題がありました。 - Issue #4145: このコミットは、具体的にGoのIssue #4145「net/http: Client.Post should follow 302/303 redirects with GET」を解決するために行われました。このIssueでは、POSTリクエストが302または303リダイレクトを受け取った際に、クライアントが自動的にGETリクエストでリダイレクトを追跡すべきであるという要望が提起されていました。
これらの背景から、Goの net/http
クライアントがより標準的なWebの慣習に準拠し、開発者がPRGパターンを容易に実装できるようにするために、このリダイレクト挙動の変更が導入されました。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
HTTPメソッド:
- GET: リソースの取得に使用されます。冪等(何度実行しても結果が変わらない)かつ安全(サーバーの状態を変更しない)であるべきです。
- POST: サーバーにデータを送信し、リソースを作成または更新するために使用されます。冪等ではないことが多く、サーバーの状態を変更します。
- HEAD: GETと同様ですが、レスポンスボディを含まず、ヘッダーのみを返します。
- PUT: 指定されたURIにリソースを作成または完全に置き換えるために使用されます。冪等です。
-
HTTPステータスコードとリダイレクト:
- 3xx (Redirection): クライアントがリクエストを完了するために、別の場所へリダイレクトする必要があることを示します。
- 301 Moved Permanently: リクエストされたリソースが恒久的に新しいURIに移動したことを示します。クライアントは将来のリクエストで新しいURIを使用すべきです。メソッドは通常維持されますが、GETに変更されることもあります。
- 302 Found (または Moved Temporarily): リクエストされたリソースが一時的に別のURIに移動したことを示します。元のメソッドを維持してリダイレクトを追跡すべきですが、歴史的に多くのクライアント(特にブラウザ)がPOSTをGETに変更してリダイレクトを追跡してきました。
- 303 See Other: リクエストのレスポンスが別のURIにあり、そのURIをGETメソッドで取得すべきであることを示します。これは、POSTリクエストの後にリソースの場所を示すために特に使用され、クライアントは常にGETメソッドでリダイレクトを追跡すべきです。
- 307 Temporary Redirect: 302と同様に一時的なリダイレクトですが、クライアントは元のリクエストメソッドを維持してリダイレクトを追跡すべきであることを厳密に指定します。
- 308 Permanent Redirect: 301と同様に恒久的なリダイレクトですが、クライアントは元のリクエストメソッドを維持してリダイレクトを追跡すべきであることを厳密に指定します。
-
Post/Redirect/Get (PRG) パターン:
- Webフォームの送信(POSTリクエスト)後に、サーバーが302または303リダイレクトを返し、クライアントを別のURL(通常は結果ページや元のフォームページ)にリダイレクトする設計パターンです。
- 目的は、ユーザーがページをリロードしたり、「戻る」ボタンを押したりしたときに、フォームの二重送信を防ぐことです。
- このパターンでは、リダイレクト先のURLへのリクエストは常にGETメソッドで行われます。
-
Go
net/http
パッケージ:- Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。
http.Client
: HTTPリクエストを送信し、レスポンスを受信するクライアントを表します。http.Request
: HTTPリクエストを表します。http.Response
: HTTPレスポンスを表します。RoundTripper
: HTTPリクエストを送信し、レスポンスを受信するインターフェース。http.Client
の内部で使用されます。
これらの知識は、コミットがなぜ特定のステータスコードに対して特定の挙動を導入したのか、そしてそれがWebのベストプラクティスにどのように合致するのかを理解する上で不可欠です。
技術的詳細
このコミットの技術的詳細は、主に src/pkg/net/http/client.go
内の Client
構造体のメソッドと、リダイレクトを処理するヘルパー関数の変更に集約されます。
-
Client.Do
メソッドの変更:- 変更前は、
req.Method == "GET" || req.Method == "HEAD"
の場合にのみc.doFollowingRedirects(req)
を呼び出していました。 - 変更後、
req.Method == "POST" || req.Method == "PUT"
の場合にもc.doFollowingRedirects(req, shouldRedirectPost)
を呼び出すようになりました。これにより、POSTおよびPUTリクエストもリダイレクト追跡の対象となります。 doFollowingRedirects
関数が、リダイレクトを追跡すべきかどうかを判断するための新しい引数shouldRedirect func(int) bool
を受け取るようになりました。
- 変更前は、
-
リダイレクト判断ロジックの分離:
- 既存の
shouldRedirect(statusCode int) bool
関数はshouldRedirectGet(statusCode int) bool
にリネームされました。これは、GET/HEADリクエストがリダイレクトを追跡すべきステータスコード(301, 302, 303, 307)を判断します。 - 新しく
shouldRedirectPost(statusCode int) bool
関数が追加されました。この関数は、POSTリクエストがリダイレクトを追跡すべきステータスコードとして、StatusFound
(302) とStatusSeeOther
(303) のみを返します。これは、PRGパターンに厳密に準拠するための重要な変更です。301や307はPOSTリクエストでは自動追跡の対象外となります。
- 既存の
-
doFollowingRedirects
メソッドの汎用化とPOSTリダイレクト時のメソッド変更:func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error)
がfunc (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error)
に変更され、リダイレクト判断ロジックを外部から注入できるようになりました。- この関数内で、リダイレクトが発生し、かつ元のリクエストメソッドがPOSTまたはPUTであった場合、リダイレクト先の新しいリクエストのメソッドが
GET
に強制的に変更されます。これは、PRGパターンにおけるPOSTリクエスト後のGETリクエストへの変換を自動的に行うための核心的な変更です。
-
Client.Post
メソッドの変更:- 変更前は、
c.send(req)
を直接呼び出していました。 - 変更後、
c.doFollowingRedirects(req, shouldRedirectPost)
を呼び出すようになりました。これにより、Post
ヘルパー関数も自動リダイレクト追跡の恩恵を受けるようになります。
- 変更前は、
-
テストケースの追加:
src/pkg/net/http/client_test.go
にTestPostRedirects
という新しいテスト関数が追加されました。- このテストは、POSTリクエストが異なるリダイレクトステータスコード(301, 302, 303, 404)を受け取った際の
net/http
クライアントの挙動を検証します。 - 特に、302と303のリダイレクトがGETリクエストとして追跡されることを確認し、ログに出力されるリクエストメソッドのシーケンスを検証することで、期待されるPRGパターンが正しく実装されていることを確認しています。
これらの変更により、Goの net/http
クライアントは、POSTリクエストに対するリダイレクト処理において、より標準的で安全なWebの慣習に準拠するようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/net/http/client.go
と src/pkg/net/http/client_test.go
にあります。
src/pkg/net/http/client.go
--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -120,7 +120,10 @@ func (c *Client) send(req *Request) (*Response, error) {
// Generally Get, Post, or PostForm will be used instead of Do.
func (c *Client) Do(req *Request) (resp *Response, err error) {
if req.Method == "GET" || req.Method == "HEAD" {
- return c.doFollowingRedirects(req)
+ return c.doFollowingRedirects(req, shouldRedirectGet)
+ }
+ if req.Method == "POST" || req.Method == "PUT" {
+ return c.doFollowingRedirects(req, shouldRedirectPost)
}
return c.send(req)
}
@@ -166,7 +169,7 @@ func send(req *Request, t RoundTripper) (resp *Response, err error) {
// True if the specified HTTP status code is one for which the Get utility should
// automatically redirect.
-func shouldRedirect(statusCode int) bool {
+func shouldRedirectGet(statusCode int) bool {
switch statusCode {
case StatusMovedPermanently, StatusFound, StatusSeeOther, StatusTemporaryRedirect:
return true
@@ -174,6 +177,16 @@ func shouldRedirect(statusCode int) bool {
return false
}
+// True if the specified HTTP status code is one for which the Post utility should
+// automatically redirect.
+func shouldRedirectPost(statusCode int) bool {
+ switch statusCode {
+ case StatusFound, StatusSeeOther:
+ return true
+ }
+ return false
+}
+
// Get issues a GET to the specified URL. If the response is one of the following
// redirect codes, Get follows the redirect, up to a maximum of 10 redirects:
//
@@ -214,10 +227,10 @@ func (c *Client) Get(url string) (resp *Response, err error) {
if err != nil {
return nil, err
}
- return c.doFollowingRedirects(req)
+ return c.doFollowingRedirects(req, shouldRedirectGet)
}
-func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error) {
+func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) {
// TODO: if/when we add cookie support, the redirected request shouldn't
// necessarily supply the same cookies as the original.
var base *url.URL
@@ -238,6 +251,9 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) {
if redirect != 0 {
req = new(Request)
req.Method = ireq.Method
+ if ireq.Method == "POST" || ireq.Method == "PUT" {
+ req.Method = "GET"
+ }
req.Header = make(Header)
req.URL, err = base.Parse(urlStr)
if err != nil {
@@ -321,7 +337,7 @@ func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Respon
return nil, err
}
req.Header.Set("Content-Type", bodyType)
- return c.send(req)
+ return c.doFollowingRedirects(req, shouldRedirectPost)
}
// PostForm issues a POST to the specified URL, with data's keys and
@@ -371,5 +387,5 @@ func (c *Client) Head(url string) (resp *Response, err error) {\n if err != nil {\n return nil, err\n }\n- return c.doFollowingRedirects(req)\n+ return c.doFollowingRedirects(req, shouldRedirectGet)\n }\n```
### `src/pkg/net/http/client_test.go`
```diff
--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -7,6 +7,7 @@
package http_test
import (
+ "bytes"
"crypto/tls"
"crypto/x509"
"errors"
@@ -246,6 +247,52 @@ func TestRedirects(t *testing.T) {\n }\n }\n \n+func TestPostRedirects(t *testing.T) {\n+\tvar log struct {\n+\t\tsync.Mutex\n+\t\tbytes.Buffer\n+\t}\n+\tvar ts *httptest.Server\n+\tts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tlog.Lock()\n+\t\tfmt.Fprintf(&log.Buffer, "%s %s ", r.Method, r.RequestURI)\n+\t\tlog.Unlock()\n+\t\tif v := r.URL.Query().Get("code"); v != "" {\n+\t\t\tcode, _ := strconv.Atoi(v)\n+\t\t\tif code/100 == 3 {\n+\t\t\t\tw.Header().Set("Location", ts.URL)\n+\t\t\t}\n+\t\t\tw.WriteHeader(code)\n+\t\t}\n+\t}))\n+\ttests := []struct {\n+\t\tsuffix string\n+\t\twant int // response code\n+\t}{\n+\t\t{"/", 200},\n+\t\t{"/?code=301", 301},\n+\t\t{"/?code=302", 200},\n+\t\t{"/?code=303", 200},\n+\t\t{"/?code=404", 404},\n+\t}\n+\tfor _, tt := range tests {\n+\t\tres, err := Post(ts.URL+tt.suffix, "text/plain", strings.NewReader("Some content"))\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tif res.StatusCode != tt.want {\n+\t\t\tt.Errorf("POST %s: status code = %d; want %d", tt.suffix, res.StatusCode, tt.want)\n+\t\t}\n+\t}\n+\tlog.Lock()\n+\tgot := log.String()\n+\tlog.Unlock()\n+\twant := "POST / POST /?code=301 POST /?code=302 GET / POST /?code=303 GET / POST /?code=404 "\n+\tif got != want {\n+\t\tt.Errorf("Log differs.\\n Got: %q\\nWant: %q", got, want)\n+\t}\n+}\n+\n var expectedCookies = []*Cookie{\n \t{Name: "ChocolateChip", Value: "tasty"},\n \t{Name: "First", Value: "Hit"},\n```
## コアとなるコードの解説
### `src/pkg/net/http/client.go` の変更点
1. **`Client.Do` メソッドの拡張**:
* 以前はGETとHEADリクエストのみが `doFollowingRedirects` を介してリダイレクトを自動追跡していました。
* 今回の変更で、POSTとPUTリクエストも `doFollowingRedirects` を呼び出すようになりました。これにより、これらのメソッドも自動リダイレクト追跡の対象となります。
* `doFollowingRedirects` に渡される引数として、リダイレクトを追跡すべきかどうかを判断する関数 (`shouldRedirectGet` または `shouldRedirectPost`) が追加されました。これは、リダイレクトの挙動をメソッドごとにカスタマイズするための重要な変更です。
2. **リダイレクト判断関数の分離と特化**:
* `shouldRedirect` 関数が `shouldRedirectGet` にリネームされ、GETリクエストに対するリダイレクトルール(301, 302, 303, 307でリダイレクト)を明確にしました。
* 新しく `shouldRedirectPost` 関数が導入されました。この関数は、POSTリクエストに対しては302 (Found) と 303 (See Other) の場合にのみ `true` を返します。これは、HTTP/1.1の仕様とPRGパターンに準拠し、POSTリクエスト後のリダイレクトでメソッドをGETに変更するべきケースを限定するためです。301 (Moved Permanently) や 307 (Temporary Redirect) は、POSTリクエストでは通常メソッドを維持してリダイレクトを追跡すべきですが、このコミットでは自動追跡の対象外としています。
3. **`doFollowingRedirects` の汎用化とPOST/PUTリダイレクト時のメソッド変更ロジック**:
* `doFollowingRedirects` 関数は、リダイレクトを追跡すべきかを判断する `shouldRedirect` 関数を引数として受け取るようになりました。これにより、この関数はGET、POST、PUTなど、様々なHTTPメソッドのリダイレクト処理に対応できるようになりました。
* 最も重要な変更は、リダイレクトが発生した際に、元のリクエストメソッドがPOSTまたはPUTであった場合、**リダイレクト先の新しいリクエストのメソッドを強制的に `GET` に変更する**ロジックが追加された点です。
```go
if ireq.Method == "POST" || ireq.Method == "PUT" {
req.Method = "GET"
}
```
このコードスニペットは、PRGパターンを自動的に実装するための核心部分です。これにより、POSTリクエストが302または303リダイレクトを受け取った場合、Goクライアントは自動的にGETリクエストでリダイレクト先のURLにアクセスし、意図しない二重送信などを防ぎます。
4. **`Client.Post` メソッドの変更**:
* `Client.Post` ヘルパー関数も、内部で `c.doFollowingRedirects(req, shouldRedirectPost)` を呼び出すように変更されました。これにより、`Post` を使用する際にも、上記で説明したPOSTリクエストのリダイレクト挙動が自動的に適用されるようになります。
### `src/pkg/net/http/client_test.go` の変更点
1. **`TestPostRedirects` の追加**:
* この新しいテスト関数は、POSTリクエストに対するリダイレクト挙動を具体的に検証するために追加されました。
* `httptest.NewServer` を使用してテスト用のHTTPサーバーを構築し、異なるステータスコード(200, 301, 302, 303, 404)を返すように設定しています。
* テストでは、`http.Post` を使用してリクエストを送信し、レスポンスのステータスコードが期待通りであるかを確認します。
* 特に重要なのは、サーバー側でリクエストメソッドとURIをログに記録し、テストの最後にそのログを検証している点です。
```go
want := "POST / POST /?code=301 POST /?code=302 GET / POST /?code=303 GET / POST /?code=404 "
```
この `want` 文字列は、以下の挙動を期待していることを示しています。
* `POST /`: 最初のPOSTリクエスト。
* `POST /?code=301`: 301リダイレクトは自動追跡されないため、元のPOSTリクエストがそのまま返される。
* `POST /?code=302 GET /`: 302リダイレクトは自動追跡され、リダイレクト先のURLへのリクエストはGETに変わる。
* `POST /?code=303 GET /`: 303リダイレクトは自動追跡され、リダイレクト先のURLへのリクエストはGETに変わる。
* `POST /?code=404`: 404はリダイレクトではないため、元のPOSTリクエストがそのまま返される。
* このテストにより、POSTリクエストに対するリダイレクト処理が、HTTPのベストプラクティス(特にPRGパターン)に沿って正しく実装されていることが保証されます。
これらの変更は、Goの `net/http` クライアントの堅牢性と使いやすさを向上させ、Webアプリケーション開発における一般的なシナリオへの対応を強化するものです。
## 関連リンク
* Go Issue #4145: [https://github.com/golang/go/issues/4145](https://github.com/golang/go/issues/4145)
* Go Change List 6923055: [https://golang.org/cl/6923055](https://golang.org/cl/6923055)
* HTTP/1.1 RFC 2616 - Section 10.3.3 302 Found: [https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3)
* HTTP/1.1 RFC 2616 - Section 10.3.4 303 See Other: [https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4)
* Post/Redirect/Get (Wikipedia): [https://en.wikipedia.org/wiki/Post/Redirect/Get](https://en.wikipedia.org/wiki/Post/Redirect/Get)
## 参考にした情報源リンク
* Go言語の公式ドキュメントとソースコード
* HTTP/1.1 RFC 2616
* Stack Overflow や技術ブログ記事(HTTPリダイレクト、Post/Redirect/Getパターンに関するもの)
* GoのIssueトラッカー(#4145)
* Goのコードレビューシステム(golang.org/cl/6923055)
# [インデックス 14626] ファイルの概要
このコミットは、Go言語の標準ライブラリである `net/http` パッケージにおいて、HTTP POSTリクエスト後のリダイレクト処理を改善するものです。具体的には、特定のHTTPステータスコード(302 Found および 303 See Other)を受け取った際に、POSTリクエストが自動的にリダイレクトされるように変更し、その際のリダイレクト先のメソッドをGETに変更する挙動を導入しています。これにより、Webアプリケーションにおける一般的な「Post/Redirect/Get」パターンへの対応が強化されます。
## コミット
commit 08ce7f1d5c309cf4187fbb1e442ee4388c29212b Author: Brad Fitzpatrick bradfitz@golang.org Date: Wed Dec 12 11:09:55 2012 -0800
net/http: follow certain redirects after POST requests
Fixes #4145
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6923055
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/08ce7f1d5c309cf4187fbb1e442ee4388c29212b](https://github.com/golang/go/commit/08ce7f1d5c309cf4187fbb1e442ee4388c29212b)
## 元コミット内容
このコミットは、Goの `net/http` パッケージにおけるHTTPクライアントのリダイレクト処理を修正するものです。特に、POSTリクエストがリダイレクトされた際の挙動に焦点を当てています。
変更前は、`http.Client` はGETまたはHEADリクエストに対してのみ自動的にリダイレクトを追跡していました。POSTリクエストがリダイレクトレスポンス(例: 301, 302, 303)を受け取った場合、クライアントは自動的にリダイレクトを追跡せず、リダイレクトレスポンスをそのまま返していました。
このコミットでは、POSTリクエストが302 (Found) または 303 (See Other) のステータスコードを受け取った場合に、自動的にリダイレクトを追跡するように変更されます。さらに重要な点として、これらのリダイレクトでは、リダイレクト先のURLへのリクエストメソッドがPOSTからGETに変更されます。これは、HTTP/1.1の仕様で推奨される「Post/Redirect/Get」パターンに準拠するための変更です。
また、この変更はGoのIssue #4145を修正するものです。
## 変更の背景
この変更の背景には、HTTPプロトコルのリダイレクトに関する一般的な慣習と、Goの `net/http` クライアントの既存の挙動との乖離がありました。
1. **HTTPリダイレクトの複雑性**: HTTPリダイレクトは、ステータスコード(301 Moved Permanently, 302 Found, 303 See Other, 307 Temporary Redirect, 308 Permanent Redirectなど)によって異なる意味と挙動を持ちます。特に、POSTリクエストがリダイレクトされる場合、その後のリクエストが元のメソッド(POST)を維持すべきか、それともGETに変更すべきかという点で、歴史的にブラウザやクライアントの実装間で一貫性がない時期がありました。
2. **Post/Redirect/Get (PRG) パターン**: Webアプリケーション開発では、フォームの送信(POSTリクエスト)後にユーザーを別のページにリダイレクトする際に、PRGパターンが広く採用されています。これは、ユーザーがブラウザの「戻る」ボタンを押した際にフォームの再送信を防ぎ、またページのリロードによって意図しない二重送信が発生するのを防ぐためのベストプラクティスです。PRGパターンでは、POSTリクエストの後に302または303リダイレクトを返し、クライアントはリダイレクト先のURLにGETリクエストを発行します。
3. **Goクライアントの既存の挙動**: このコミット以前のGoの `net/http` クライアントは、POSTリクエストに対するリダイレクトを自動的に追跡しませんでした。これは、PRGパターンを実装する際に開発者が手動でリダイレクトを処理する必要があることを意味し、利便性が低いという問題がありました。
4. **Issue #4145**: このコミットは、具体的にGoのIssue #4145「net/http: Client.Post should follow 302/303 redirects with GET」を解決するために行われました。このIssueでは、POSTリクエストが302または303リダイレクトを受け取った際に、クライアントが自動的にGETリクエストでリダイレクトを追跡すべきであるという要望が提起されていました。
これらの背景から、Goの `net/http` クライアントがより標準的なWebの慣習に準拠し、開発者がPRGパターンを容易に実装できるようにするために、このリダイレクト挙動の変更が導入されました。
## 前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. **HTTPメソッド**:
* **GET**: リソースの取得に使用されます。冪等(何度実行しても結果が変わらない)かつ安全(サーバーの状態を変更しない)であるべきです。
* **POST**: サーバーにデータを送信し、リソースを作成または更新するために使用されます。冪等ではないことが多く、サーバーの状態を変更します。
* **HEAD**: GETと同様ですが、レスポンスボディを含まず、ヘッダーのみを返します。
* **PUT**: 指定されたURIにリソースを作成または完全に置き換えるために使用されます。冪等です。
2. **HTTPステータスコードとリダイレクト**:
* **3xx (Redirection)**: クライアントがリクエストを完了するために、別の場所へリダイレクトする必要があることを示します。
* **301 Moved Permanently**: リクエストされたリソースが恒久的に新しいURIに移動したことを示します。クライアントは将来のリクエストで新しいURIを使用すべきです。メソッドは通常維持されますが、GETに変更されることもあります。
* **302 Found (または Moved Temporarily)**: リクエストされたリソースが一時的に別のURIに移動したことを示します。元のメソッドを維持してリダイレクトを追跡すべきですが、歴史的に多くのクライアント(特にブラウザ)がPOSTをGETに変更してリダイレクトを追跡してきました。
* **303 See Other**: リクエストのレスポンスが別のURIにあり、そのURIをGETメソッドで取得すべきであることを示します。これは、POSTリクエストの後にリソースの場所を示すために特に使用され、クライアントは常にGETメソッドでリダイレクトを追跡すべきです。
* **307 Temporary Redirect**: 302と同様に一時的なリダイレクトですが、クライアントは元のリクエストメソッドを維持してリダイレクトを追跡すべきであることを厳密に指定します。
* **308 Permanent Redirect**: 301と同様に恒久的なリダイレクトですが、クライアントは元のリクエストメソッドを維持してリダイレクトを追跡すべきであることを厳密に指定します。
3. **Post/Redirect/Get (PRG) パターン**:
* Webフォームの送信(POSTリクエスト)後に、サーバーが302または303リダイレクトを返し、クライアントを別のURL(通常は結果ページや元のフォームページ)にリダイレクトする設計パターンです。
* 目的は、ユーザーがページをリロードしたり、「戻る」ボタンを押したりしたときに、フォームの二重送信を防ぐことです。
* このパターンでは、リダイレクト先のURLへのリクエストは常にGETメソッドで行われます。
4. **Go `net/http` パッケージ**:
* Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。
* `http.Client`: HTTPリクエストを送信し、レスポンスを受信するクライアントを表します。
* `http.Request`: HTTPリクエストを表します。
* `http.Response`: HTTPレスポンスを表します。
* `RoundTripper`: HTTPリクエストを送信し、レスポンスを受信するインターフェース。`http.Client` の内部で使用されます。
これらの知識は、コミットがなぜ特定のステータスコードに対して特定の挙動を導入したのか、そしてそれがWebのベストプラクティスにどのように合致するのかを理解する上で不可欠です。
## 技術的詳細
このコミットの技術的詳細は、主に `src/pkg/net/http/client.go` 内の `Client` 構造体のメソッドと、リダイレクトを処理するヘルパー関数の変更に集約されます。
1. **`Client.Do` メソッドの変更**:
* 変更前は、`req.Method == "GET" || req.Method == "HEAD"` の場合にのみ `c.doFollowingRedirects(req)` を呼び出していました。
* 変更後、`req.Method == "POST" || req.Method == "PUT"` の場合にも `c.doFollowingRedirects(req, shouldRedirectPost)` を呼び出すようになりました。これにより、POSTおよびPUTリクエストもリダイレクト追跡の対象となります。
* `doFollowingRedirects` 関数が、リダイレクトを追跡すべきかどうかを判断するための新しい引数 `shouldRedirect func(int) bool` を受け取るようになりました。
2. **リダイレクト判断ロジックの分離**:
* 既存の `shouldRedirect(statusCode int) bool` 関数は `shouldRedirectGet(statusCode int) bool` にリネームされました。これは、GET/HEADリクエストがリダイレクトを追跡すべきステータスコード(301, 302, 303, 307)を判断します。
* 新しく `shouldRedirectPost(statusCode int) bool` 関数が追加されました。この関数は、POSTリクエストがリダイレクトを追跡すべきステータスコードとして、`StatusFound` (302) と `StatusSeeOther` (303) のみを返します。これは、PRGパターンに厳密に準拠するための重要な変更です。301や307はPOSTリクエストでは自動追跡の対象外となります。
3. **`doFollowingRedirects` メソッドの汎用化とPOSTリダイレクト時のメソッド変更**:
* `func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error)` が `func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error)` に変更され、リダイレクト判断ロジックを外部から注入できるようになりました。
* この関数内で、リダイレクトが発生し、かつ元のリクエストメソッドがPOSTまたはPUTであった場合、リダイレクト先の新しいリクエストのメソッドが `GET` に強制的に変更されます。これは、PRGパターンにおけるPOSTリクエスト後のGETリクエストへの変換を自動的に行うための核心的な変更です。
4. **`Client.Post` メソッドの変更**:
* 変更前は、`c.send(req)` を直接呼び出していました。
* 変更後、`c.doFollowingRedirects(req, shouldRedirectPost)` を呼び出すようになりました。これにより、`Post` ヘルパー関数も自動リダイレクト追跡の恩恵を受けるようになります。
5. **テストケースの追加**:
* `src/pkg/net/http/client_test.go` に `TestPostRedirects` という新しいテスト関数が追加されました。
* このテストは、POSTリクエストが異なるリダイレクトステータスコード(301, 302, 303, 404)を受け取った際の `net/http` クライアントの挙動を検証します。
* 特に、302と303のリダイレクトがGETリクエストとして追跡されることを確認し、ログに出力されるリクエストメソッドのシーケンスを検証することで、期待されるPRGパターンが正しく実装されていることを確認しています。
これらの変更により、Goの `net/http` クライアントは、POSTリクエストに対するリダイレクト処理において、より標準的で安全なWebの慣習に準拠するようになりました。
## コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に `src/pkg/net/http/client.go` と `src/pkg/net/http/client_test.go` にあります。
### `src/pkg/net/http/client.go`
```diff
--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -120,7 +120,10 @@ func (c *Client) send(req *Request) (*Response, error) {
// Generally Get, Post, or PostForm will be used instead of Do.
func (c *Client) Do(req *Request) (resp *Response, err error) {
if req.Method == "GET" || req.Method == "HEAD" {
- return c.doFollowingRedirects(req)
+ return c.doFollowingRedirects(req, shouldRedirectGet)
+ }
+ if req.Method == "POST" || req.Method == "PUT" {
+ return c.doFollowingRedirects(req, shouldRedirectPost)
}
return c.send(req)
}
@@ -166,7 +169,7 @@ func send(req *Request, t RoundTripper) (resp *Response, err error) {
// True if the specified HTTP status code is one for which the Get utility should
// automatically redirect.
-func shouldRedirect(statusCode int) bool {
+func shouldRedirectGet(statusCode int) bool {
switch statusCode {
case StatusMovedPermanently, StatusFound, StatusSeeOther, StatusTemporaryRedirect:
return true
@@ -174,6 +177,16 @@ func shouldRedirect(statusCode int) bool {
return false
}
+// True if the specified HTTP status code is one for which the Post utility should
+// automatically redirect.
+func shouldRedirectPost(statusCode int) bool {
+ switch statusCode {
+ case StatusFound, StatusSeeOther:
+ return true
+ }
+ return false
+}
+
// Get issues a GET to the specified URL. If the response is one of the following
// redirect codes, Get follows the redirect, up to a maximum of 10 redirects:
//
@@ -214,10 +227,10 @@ func (c *Client) Get(url string) (resp *Response, err error) {
if err != nil {
return nil, err
}
- return c.doFollowingRedirects(req)
+ return c.doFollowingRedirects(req, shouldRedirectGet)
}
-func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error) {
+func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) {
// TODO: if/when we add cookie support, the redirected request shouldn't
// necessarily supply the same cookies as the original.
var base *url.URL
@@ -238,6 +251,9 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) {
if redirect != 0 {
req = new(Request)
req.Method = ireq.Method
+ if ireq.Method == "POST" || ireq.Method == "PUT" {
+ req.Method = "GET"
+ }
req.Header = make(Header)
req.URL, err = base.Parse(urlStr)
if err != nil {
@@ -321,7 +337,7 @@ func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Respon
return nil, err
}
req.Header.Set("Content-Type", bodyType)
- return c.send(req)
+ return c.doFollowingRedirects(req, shouldRedirectPost)
}
// PostForm issues a POST to the specified URL, with data's keys and
@@ -371,5 +387,5 @@ func (c *Client) Head(url string) (resp *Response, err error) {\n if err != nil {\n return nil, err\n }\n- return c.doFollowingRedirects(req)\n+ return c.doFollowingRedirects(req, shouldRedirectGet)\n }\n```
### `src/pkg/net/http/client_test.go`
```diff
--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -7,6 +7,7 @@
package http_test
import (
+ "bytes"
"crypto/tls"
"crypto/x509"
"errors"
@@ -246,6 +247,52 @@ func TestRedirects(t *testing.T) {\n }\n }\n \n+func TestPostRedirects(t *testing.T) {\n+\tvar log struct {\n+\t\tsync.Mutex\n+\t\tbytes.Buffer\n+\t}\n+\tvar ts *httptest.Server\n+\tts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tlog.Lock()\n+\t\tfmt.Fprintf(&log.Buffer, "%s %s ", r.Method, r.RequestURI)\n+\t\tlog.Unlock()\n+\t\tif v := r.URL.Query().Get("code"); v != "" {\n+\t\t\tcode, _ := strconv.Atoi(v)\n+\t\t\tif code/100 == 3 {\n+\t\t\t\tw.Header().Set("Location", ts.URL)\n+\t\t\t}\n+\t\t\tw.WriteHeader(code)\n+\t\t}\n+\t}))\n+\ttests := []struct {\n+\t\tsuffix string\n+\t\twant int // response code\n+\t}{\n+\t\t{"/", 200},\n+\t\t{"/?code=301", 301},\n+\t\t{"/?code=302", 200},\n+\t\t{"/?code=303", 200},\n+\t\t{"/?code=404", 404},\n+\t}\n+\tfor _, tt := range tests {\n+\t\tres, err := Post(ts.URL+tt.suffix, "text/plain", strings.NewReader("Some content"))\n+\t\tif err != nil {\n+\t\t\tt.Fatal(err)\n+\t\t}\n+\t\tif res.StatusCode != tt.want {\n+\t\t\tt.Errorf("POST %s: status code = %d; want %d", tt.suffix, res.StatusCode, tt.want)\n+\t\t}\n+\t}\n+\tlog.Lock()\n+\tgot := log.String()\n+\tlog.Unlock()\n+\twant := "POST / POST /?code=301 POST /?code=302 GET / POST /?code=303 GET / POST /?code=404 "\n+\tif got != want {\n+\t\tt.Errorf("Log differs.\\n Got: %q\\nWant: %q", got, want)\n+\t}\n+}\n+\n var expectedCookies = []*Cookie{\n \t{Name: "ChocolateChip", Value: "tasty"},\n \t{Name: "First", Value: "Hit"},\n```
## コアとなるコードの解説
### `src/pkg/net/http/client.go` の変更点
1. **`Client.Do` メソッドの拡張**:
* 以前はGETとHEADリクエストのみが `doFollowingRedirects` を介してリダイレクトを自動追跡していました。
* 今回の変更で、POSTとPUTリクエストも `doFollowingRedirects` を呼び出すようになりました。これにより、これらのメソッドも自動リダイレクト追跡の対象となります。
* `doFollowingRedirects` に渡される引数として、リダイレクトを追跡すべきかどうかを判断する関数 (`shouldRedirectGet` または `shouldRedirectPost`) が追加されました。これは、リダイレクトの挙動をメソッドごとにカスタマイズするための重要な変更です。
2. **リダイレクト判断関数の分離と特化**:
* `shouldRedirect` 関数が `shouldRedirectGet` にリネームされ、GETリクエストに対するリダイレクトルール(301, 302, 303, 307でリダイレクト)を明確にしました。
* 新しく `shouldRedirectPost` 関数が導入されました。この関数は、POSTリクエストに対しては302 (Found) と 303 (See Other) の場合にのみ `true` を返します。これは、HTTP/1.1の仕様とPRGパターンに準拠し、POSTリクエスト後のリダイレクトでメソッドをGETに変更するべきケースを限定するためです。301 (Moved Permanently) や 307 (Temporary Redirect) は、POSTリクエストでは通常メソッドを維持してリダイレクトを追跡すべきですが、このコミットでは自動追跡の対象外としています。
3. **`doFollowingRedirects` の汎用化とPOST/PUTリダイレクト時のメソッド変更ロジック**:
* `doFollowingRedirects` 関数は、リダイレクトを追跡すべきかを判断する `shouldRedirect` 関数を引数として受け取るようになりました。これにより、この関数はGET、POST、PUTなど、様々なHTTPメソッドのリダイレクト処理に対応できるようになりました。
* 最も重要な変更は、リダイレクトが発生した際に、元のリクエストメソッドがPOSTまたはPUTであった場合、**リダイレクト先の新しいリクエストのメソッドを強制的に `GET` に変更する**ロジックが追加された点です。
```go
if ireq.Method == "POST" || ireq.Method == "PUT" {
req.Method = "GET"
}
```
このコードスニペットは、PRGパターンを自動的に実装するための核心部分です。これにより、POSTリクエストが302または303リダイレクトを受け取った場合、Goクライアントは自動的にGETリクエストでリダイレクト先のURLにアクセスし、意図しない二重送信などを防ぎます。
4. **`Client.Post` メソッドの変更**:
* `Client.Post` ヘルパー関数も、内部で `c.doFollowingRedirects(req, shouldRedirectPost)` を呼び出すように変更されました。これにより、`Post` を使用する際にも、上記で説明したPOSTリクエストのリダイレクト挙動が自動的に適用されるようになります。
### `src/pkg/net/http/client_test.go` の変更点
1. **`TestPostRedirects` の追加**:
* この新しいテスト関数は、POSTリクエストに対するリダイレクト挙動を具体的に検証するために追加されました。
* `httptest.NewServer` を使用してテスト用のHTTPサーバーを構築し、異なるステータスコード(200, 301, 302, 303, 404)を返すように設定しています。
* テストでは、`http.Post` を使用してリクエストを送信し、レスポンスのステータスコードが期待通りであるかを確認します。
* 特に重要なのは、サーバー側でリクエストメソッドとURIをログに記録し、テストの最後にそのログを検証している点です。
```go
want := "POST / POST /?code=301 POST /?code=302 GET / POST /?code=303 GET / POST /?code=404 "
```
この `want` 文字列は、以下の挙動を期待していることを示しています。
* `POST /`: 最初のPOSTリクエスト。
* `POST /?code=301`: 301リダイレクトは自動追跡されないため、元のPOSTリクエストがそのまま返される。
* `POST /?code=302 GET /`: 302リダイレクトは自動追跡され、リダイレクト先のURLへのリクエストはGETに変わる。
* `POST /?code=303 GET /`: 303リダイレクトは自動追跡され、リダイレクト先のURLへのリクエストはGETに変わる。
* `POST /?code=404`: 404はリダイレクトではないため、元のPOSTリクエストがそのまま返される。
* このテストにより、POSTリクエストに対するリダイレクト処理が、HTTPのベストプラクティス(特にPRGパターン)に沿って正しく実装されていることが保証されます。
これらの変更は、Goの `net/http` クライアントの堅牢性と使いやすさを向上させ、Webアプリケーション開発における一般的なシナリオへの対応を強化するものです。
## 関連リンク
* Go Issue #4145: [https://github.com/golang/go/issues/4145](https://github.com/golang/go/issues/4145)
* Go Change List 6923055: [https://golang.org/cl/6923055](https://golang.org/cl/6923055)
* HTTP/1.1 RFC 2616 - Section 10.3.3 302 Found: [https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3)
* HTTP/1.1 RFC 2616 - Section 10.3.4 303 See Other: [https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4)
* Post/Redirect/Get (Wikipedia): [https://en.wikipedia.org/wiki/Post/Redirect/Get](https://en.wikipedia.org/wiki/Post/Redirect/Get)
## 参考にした情報源リンク
* Go言語の公式ドキュメントとソースコード
* HTTP/1.1 RFC 2616
* Stack Overflow や技術ブログ記事(HTTPリダイレクト、Post/Redirect/Getパターンに関するもの)
* GoのIssueトラッカー(#4145)
* Goのコードレビューシステム(golang.org/cl/6923055)