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

[インデックス 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の初期の動作に戻すものです。具体的には、ClientCheckRedirect 関数がエラーを返した場合に、nil でない *http.Responsenil でないエラーの両方を返すように変更しています。これは、以前の変更によって失われたGo 1の互換性のある動作を回復させることを目的としています。また、この修正された動作を検証するためのテストケースも追加されています。

変更の背景

この変更は、Go言語のIssue #3795 (https://golang.org/issue/3795) を修正するために行われました。

Go 1の初期の net/http パッケージでは、Client.CheckRedirect 関数がエラーを返した場合、ClientGet メソッド(やその他のリクエスト送信メソッド)は、リダイレクトが失敗する直前に受け取った *http.Response オブジェクトと、CheckRedirect が返したエラーの両方を呼び出し元に返していました。この挙動は、リダイレクトチェーンの途中で何らかの問題が発生した場合でも、直前のレスポンスヘッダ(特に Location ヘッダなど)を検査したいというユースケースにおいて有用でした。

しかし、その後の変更(おそらくパフォーマンス最適化やコードの簡素化のため)により、CheckRedirect がエラーを返した場合に *http.Responsenil になってしまうという回帰バグが発生しました。これにより、ユーザーはリダイレクト失敗時の詳細なコンテキスト(例えば、どの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.Responseurl.Error を返していました。これは、リダイレクトが失敗した際に、その直前の有効なレスポンスオブジェクト(特に、リダイレクト先のURLを示す Location ヘッダを含むレスポンス)が失われることを意味しました。

このコミットでは、この問題を解決するために以下の変更が加えられています。

  1. redirectFailed フラグの導入: doFollowingRedirects 関数内に redirectFailed := false という新しいブール変数が導入されました。
  2. フラグの設定: CheckRedirect 関数(内部的には redirectChecker を通じて呼び出される)がエラーを返した場合、この redirectFailed フラグが true に設定されます。
  3. 条件付きの戻り値: doFollowingRedirects 関数のエラー処理部分で、redirectFailed フラグが true であるかどうかをチェックする条件分岐が追加されました。
    • もし redirectFailedtrue であれば、関数は resp (リダイレクト失敗直前の *http.Response オブジェクト) と url.Error の両方を返します。この際、resp.Body.Close() は行われません。これにより、呼び出し元は resp オブジェクトにアクセスし、そのヘッダ(例: Location)を読み取ることができます。
    • もし redirectFailedfalse であれば(つまり、エラーが CheckRedirect 以外の原因で発生した場合)、以前と同様に resp.Body.Close() を呼び出し、nil*http.Responseurl.Error を返します。

この変更により、CheckRedirect がエラーを返した場合でも、リダイレクトが中止された時点での最後の有効なレスポンスオブジェクトが呼び出し元に提供されるようになり、Go 1の初期の互換性のある動作が回復されました。

また、この修正された動作を検証するために、client_test.go に新しいテストアサーションが追加されています。これにより、CheckRedirect がエラーを返した際に *http.Responsenil でないこと、およびそのレスポンスに 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 の変更点

  1. 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の初期の)挙動を明記しています。
  2. doFollowingRedirects 関数内の変更:

    • redirectFailed := false の追加: リダイレクトが CheckRedirect のエラーによって失敗したかどうかを追跡するためのフラグが導入されました。
    • if err != nil { redirectFailed = true; break } の追加: redirectChecker (内部で CheckRedirect を呼び出す) がエラーを返した場合、redirectFailed フラグを true に設定し、リダイレクトループを中断します。
    • エラーハンドリングロジックの変更:
      • 変更前は、respnil でない場合に resp.Body.Close() を呼び出し、常に nil*Responseurl.Error を返していました。
      • 変更後、urlErr という変数に url.Error を格納した後、if redirectFailed の条件分岐が追加されました。
      • if redirectFailedtrue の場合、resp (リダイレクト失敗直前のレスポンス) と urlErr をそのまま返します。このパスでは resp.Body.Close() は呼び出されません。これにより、呼び出し元は resp オブジェクトにアクセスできます。
      • if redirectFailedfalse の場合(つまり、CheckRedirect 以外でエラーが発生した場合)、以前と同様に resp.Body.Close() を呼び出し、nil*ResponseurlErr を返します。

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およびChange-Listのページ
  • Go言語の net/http パッケージのドキュメント (Go 1.0当時のものを含む)
  • HTTPリダイレクトに関する一般的な知識
  • Go言語のテストフレームワーク testing に関する知識