[インデックス 13481] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージにおける Client のリダイレクト処理の挙動を、Go 1の初期の動作に戻すものです。具体的には、Client.CheckRedirect 関数がエラーを返した場合に、以前の *http.Response オブジェクトとエラーの両方を返すように修正し、その動作を検証するテストを追加しています。
コミット
commit dfd7f18130e538c53a2974988caecacd53d473f1
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jul 18 13:48:39 2012 -0700
net/http: revert back to (and test) Go 1 CheckRedirect behavior
If a Client's CheckRedirect function returns an error, we
again return both a non-nil *Response and a non-nil error.
Fixes #3795
R=golang-dev, n13m3y3r
CC=golang-dev
https://golang.org/cl/6429044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dfd7f18130e538c53a2974988caecacd53d473f1
元コミット内容
このコミットは、net/http パッケージの Client におけるリダイレクト処理の挙動を、Go 1の初期の動作に戻すものです。具体的には、Client の CheckRedirect 関数がエラーを返した場合に、nil でない *http.Response と nil でないエラーの両方を返すように変更しています。これは、以前の変更によって失われたGo 1の互換性のある動作を回復させることを目的としています。また、この修正された動作を検証するためのテストケースも追加されています。
変更の背景
この変更は、Go言語のIssue #3795 (https://golang.org/issue/3795) を修正するために行われました。
Go 1の初期の net/http パッケージでは、Client.CheckRedirect 関数がエラーを返した場合、Client の Get メソッド(やその他のリクエスト送信メソッド)は、リダイレクトが失敗する直前に受け取った *http.Response オブジェクトと、CheckRedirect が返したエラーの両方を呼び出し元に返していました。この挙動は、リダイレクトチェーンの途中で何らかの問題が発生した場合でも、直前のレスポンスヘッダ(特に Location ヘッダなど)を検査したいというユースケースにおいて有用でした。
しかし、その後の変更(おそらくパフォーマンス最適化やコードの簡素化のため)により、CheckRedirect がエラーを返した場合に *http.Response が nil になってしまうという回帰バグが発生しました。これにより、ユーザーはリダイレクト失敗時の詳細なコンテキスト(例えば、どのURLへのリダイレクトが拒否されたかを示す Location ヘッダなど)にアクセスできなくなり、デバッグやエラーハンドリングが困難になりました。
このコミットは、この回帰バグを修正し、Go 1の初期の期待される動作、すなわち CheckRedirect がエラーを返した場合に *http.Response とエラーの両方を返す挙動を回復させるものです。
前提知識の解説
Go言語の net/http パッケージ
net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。
http.Client: HTTPリクエストを送信し、レスポンスを受信するクライアントを表します。通常、http.DefaultClientを使用するか、カスタム設定を持つhttp.Clientインスタンスを作成します。http.Request: 送信するHTTPリクエストを表します。メソッド(GET, POSTなど)、URL、ヘッダ、ボディなどの情報を含みます。http.Response: 受信したHTTPレスポンスを表します。ステータスコード、ヘッダ、ボディなどの情報を含みます。- リダイレクト (Redirect): HTTPプロトコルにおいて、サーバーがクライアントに対して別のURLへリクエストを再送信するよう指示する仕組みです。通常、3xx系のステータスコード(例: 301 Moved Permanently, 302 Found, 307 Temporary Redirect)で応答されます。
Client.CheckRedirectフィールド:http.Client構造体のフィールドの一つで、リダイレクトを処理する際のポリシーをカスタマイズするために使用されます。これはfunc(req *Request, via []*Request) error型の関数ポインタです。req: 次にリダイレクトされるリクエスト(まだ送信されていないもの)。via: これまでに経由してきたリクエストのリスト(古いものから順)。- この関数が
nilでない場合、Clientはリダイレクトを追跡する前にこの関数を呼び出します。 - この関数がエラーを返すと、
Clientはそのエラーを返してリダイレクトの追跡を中止します。 - この関数が
nilを返すと、Clientはリダイレクトを追跡します。 - このフィールドが
nilの場合、Clientはデフォルトのリダイレクトポリシー(最大10回のリダイレクトを自動的に追跡し、異なるホストへのPOSTリクエストはGETに変換する)を使用します。
url.Error
url.Error は、net/url パッケージで定義されているエラー型で、URLの処理中に発生したエラーをラップするために使用されます。これには、エラーが発生した操作 (Op)、関連するURL (URL)、および元のエラー (Err) の情報が含まれます。net/http パッケージでは、HTTPリクエストの送信やリダイレクト処理中に発生したエラーを url.Error でラップして返すことがよくあります。
技術的詳細
このコミットの技術的な核心は、net/http パッケージの Client がリダイレクトを処理する内部関数 doFollowingRedirects の挙動変更にあります。
以前のバグのある実装では、Client.CheckRedirect がエラーを返してリダイレクトが中止された場合、doFollowingRedirects 関数は最終的に nil の *http.Response と url.Error を返していました。これは、リダイレクトが失敗した際に、その直前の有効なレスポンスオブジェクト(特に、リダイレクト先のURLを示す Location ヘッダを含むレスポンス)が失われることを意味しました。
このコミットでは、この問題を解決するために以下の変更が加えられています。
redirectFailedフラグの導入:doFollowingRedirects関数内にredirectFailed := falseという新しいブール変数が導入されました。- フラグの設定:
CheckRedirect関数(内部的にはredirectCheckerを通じて呼び出される)がエラーを返した場合、このredirectFailedフラグがtrueに設定されます。 - 条件付きの戻り値:
doFollowingRedirects関数のエラー処理部分で、redirectFailedフラグがtrueであるかどうかをチェックする条件分岐が追加されました。- もし
redirectFailedがtrueであれば、関数はresp(リダイレクト失敗直前の*http.Responseオブジェクト) とurl.Errorの両方を返します。この際、resp.Body.Close()は行われません。これにより、呼び出し元はrespオブジェクトにアクセスし、そのヘッダ(例:Location)を読み取ることができます。 - もし
redirectFailedがfalseであれば(つまり、エラーがCheckRedirect以外の原因で発生した場合)、以前と同様にresp.Body.Close()を呼び出し、nilの*http.Responseとurl.Errorを返します。
- もし
この変更により、CheckRedirect がエラーを返した場合でも、リダイレクトが中止された時点での最後の有効なレスポンスオブジェクトが呼び出し元に提供されるようになり、Go 1の初期の互換性のある動作が回復されました。
また、この修正された動作を検証するために、client_test.go に新しいテストアサーションが追加されています。これにより、CheckRedirect がエラーを返した際に *http.Response が nil でないこと、およびそのレスポンスに Location ヘッダが含まれていることが保証されます。
コアとなるコードの変更箇所
src/pkg/net/http/client.go
--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -33,10 +33,11 @@ type Client struct {
// CheckRedirect specifies the policy for handling redirects.
// If CheckRedirect is not nil, the client calls it before
- // following an HTTP redirect. The arguments req and via
- // are the upcoming request and the requests made already,
- // oldest first. If CheckRedirect returns an error, the client
- // returns that error (wrapped in a url.Error) instead of
+ // following an HTTP redirect. The arguments req and via are
+ // the upcoming request and the requests made already, oldest
+ // first. If CheckRedirect returns an error, the Client's Get
+ // method returns both the previous Response and
+ // CheckRedirect's error (wrapped in a url.Error) instead of
// issuing the Request req.
//
// If CheckRedirect is nil, the Client uses its default policy,
@@ -221,6 +222,7 @@ func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error)
req := ireq
urlStr := "" // next relative or absolute URL to fetch (after first request)
+ redirectFailed := false
for redirect := 0; ; redirect++ {
if redirect != 0 {
req = new(Request)
@@ -239,6 +241,7 @@ func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error)
err = redirectChecker(req, via)
if err != nil {
+ redirectFailed = true
break
}
}
@@ -268,16 +271,24 @@ func (c *Client) doFollowingRedirects(ireq *Request) (resp *Response, err error)
return
}
- if resp != nil {
- resp.Body.Close()
- }
-
method := ireq.Method
- return nil, &url.Error{
+ urlErr := &url.Error{
Op: method[0:1] + strings.ToLower(method[1:]),
URL: urlStr,
Err: err,
}
+
+ if redirectFailed {
+ // Special case for Go 1 compatibility: return both the response
+ // and an error if the CheckRedirect function failed.
+ // See http://golang.org/issue/3795
+ return resp, urlErr
+ }
+
+ if resp != nil {
+ resp.Body.Close()
+ }
+ return nil, urlErr
}
func defaultCheckRedirect(req *Request, via []*Request) error {
src/pkg/net/http/client_test.go
--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -234,6 +234,12 @@ func TestRedirects(t *testing.T) {
if urlError, ok := err.(*url.Error); !ok || urlError.Err != checkErr {
t.Errorf("with redirects forbidden, expected a *url.Error with our 'no redirects allowed' error inside; got %#v (%q)", err, err)
}
+ if res == nil {
+ t.Fatalf("Expected a non-nil Response on CheckRedirect failure (http://golang.org/issue/3795)")
+ }
+ if res.Header.Get("Location") == "" {
+ t.Errorf("no Location header in Response")
+ }
}
var expectedCookies = []*Cookie{
コアとなるコードの解説
src/pkg/net/http/client.go の変更点
-
Client.CheckRedirectのコメント更新:- 変更前:
If CheckRedirect returns an error, the client returns that error (wrapped in a url.Error) instead of issuing the Request req. - 変更後:
If CheckRedirect returns an error, the Client's Get method returns both the previous Response and CheckRedirect's error (wrapped in a url.Error) instead of issuing the Request req. - このコメントの変更は、このコミットの意図を明確に示しています。
CheckRedirectがエラーを返した場合に、*Responseとエラーの両方が返されるという新しい(Go 1の初期の)挙動を明記しています。
- 変更前:
-
doFollowingRedirects関数内の変更:redirectFailed := falseの追加: リダイレクトがCheckRedirectのエラーによって失敗したかどうかを追跡するためのフラグが導入されました。if err != nil { redirectFailed = true; break }の追加:redirectChecker(内部でCheckRedirectを呼び出す) がエラーを返した場合、redirectFailedフラグをtrueに設定し、リダイレクトループを中断します。- エラーハンドリングロジックの変更:
- 変更前は、
respがnilでない場合にresp.Body.Close()を呼び出し、常にnilの*Responseとurl.Errorを返していました。 - 変更後、
urlErrという変数にurl.Errorを格納した後、if redirectFailedの条件分岐が追加されました。 if redirectFailedがtrueの場合、resp(リダイレクト失敗直前のレスポンス) とurlErrをそのまま返します。このパスではresp.Body.Close()は呼び出されません。これにより、呼び出し元はrespオブジェクトにアクセスできます。if redirectFailedがfalseの場合(つまり、CheckRedirect以外でエラーが発生した場合)、以前と同様にresp.Body.Close()を呼び出し、nilの*ResponseとurlErrを返します。
- 変更前は、
src/pkg/net/http/client_test.go の変更点
TestRedirects 関数内に、CheckRedirect がエラーを返した際の挙動を検証する新しいアサーションが追加されました。
if res == nil { t.Fatalf("Expected a non-nil Response on CheckRedirect failure (http://golang.org/issue/3795)") }:- このアサーションは、
CheckRedirectがエラーを返した場合でも、doFollowingRedirects関数がnilでない*http.Responseオブジェクトを返すことを保証します。これは、Issue #3795 で報告された回帰バグの直接的な修正を検証するものです。
- このアサーションは、
if res.Header.Get("Location") == "" { t.Errorf("no Location header in Response") }:- このアサーションは、返された
*http.Responseオブジェクトに、リダイレクト先のURLを示すLocationヘッダが含まれていることを確認します。これは、ユーザーがリダイレクト失敗時にどのURLへのリダイレクトが拒否されたかを特定できるようにするために重要です。
- このアサーションは、返された
これらの変更により、net/http クライアントのリダイレクト処理は、Go 1の初期の期待される動作に戻り、より堅牢なエラーハンドリングと情報提供が可能になりました。
関連リンク
- Go Issue #3795:
net/http: CheckRedirect error returns nil Response- https://golang.org/issue/3795 - Go Change-List 6429044:
net/http: revert back to (and test) Go 1 CheckRedirect behavior- https://golang.org/cl/6429044
参考にした情報源リンク
- 上記のGo IssueおよびChange-Listのページ
- Go言語の
net/httpパッケージのドキュメント (Go 1.0当時のものを含む) - HTTPリダイレクトに関する一般的な知識
- Go言語のテストフレームワーク
testingに関する知識