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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HTTPトランスポート層で発生するエラー、特にタイムアウトエラーの扱いを改善するものです。具体的には、responseAndError 構造体が net.Error インターフェースを満たすように変更され、クライアントがエラー文字列の比較に頼ることなく、タイムアウトエラーをプログラム的に検出できるようになります。

コミット

commit 5e711b473c7aafd47dd0a3c3e66ceaa5bf07435b
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date:   Wed Feb 12 07:59:58 2014 -0800

    net/http: make responseAndError satisfy the net.Error interface
    
    Allow clients to check for timeouts without relying on error substring
    matching.
    
    Fixes #6185.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/55470048

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

https://github.com/golang/go/commit/5e711b473c7aafd47dd0a3c3e66ceaa5bf07435b

元コミット内容

このコミットの元の内容は以下の通りです。

net/http: make responseAndError satisfy the net.Error interface

Allow clients to check for timeouts without relying on error substring
matching.

Fixes #6185.

変更の背景

Go言語の net/http パッケージにおいて、HTTPリクエストの処理中に発生するエラー、特にネットワーク関連のエラー(タイムアウトなど)は、これまで単なる error インターフェースとして返されていました。これにより、クライアントコードが特定のエラータイプ(例えばタイムアウト)を識別するためには、エラーメッセージの文字列を解析(部分文字列の一致確認など)する必要がありました。これは脆弱で、エラーメッセージの変更によってコードが壊れる可能性があり、また国際化対応も困難でした。

この問題は、GoのIssue #6185で報告されており、タイムアウトエラーをプログラム的に識別できるようにすることが求められていました。net.Error インターフェースは、ネットワーク操作に関連するエラーに対して、一時的なエラーであるか、タイムアウトであるかといった追加情報を提供するメソッド(Temporary()Timeout())を定義しています。このコミットの目的は、net/http パッケージが返すタイムアウトエラーがこの net.Error インターフェースを満たすようにすることで、より堅牢でタイプセーフなエラーハンドリングを可能にすることです。

前提知識の解説

Go言語のエラーハンドリング

Go言語では、エラーは組み込みの error インターフェースによって表現されます。このインターフェースは Error() string メソッドのみを定義しており、エラーの文字列表現を返します。

type error interface {
    Error() string
}

net.Error インターフェース

net パッケージ(net/http が依存する低レベルのネットワークパッケージ)には、より具体的なネットワークエラー情報を提供する net.Error インターフェースが定義されています。

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

このインターフェースを実装することで、エラーがタイムアウトによるものか (Timeout() bool)、あるいは一時的なもので再試行可能か (Temporary() bool) を、エラーメッセージの文字列解析に頼ることなくプログラム的に判断できるようになります。

net/http パッケージの Transport

net/http パッケージの Transport は、HTTPリクエストの実際の送信とレスポンスの受信を担当する低レベルのコンポーネントです。これには、コネクションの再利用、プロキシのサポート、TLSハンドシェイクなどが含まれます。Transport は、リクエストのタイムアウト処理も行います。

responseAndError 構造体

コミット前の net/http パッケージ内部では、HTTPレスポンスとそれに伴うエラーをカプセル化するために responseAndError のような内部構造体が使用されていました。この構造体は、HTTPリクエストの処理結果を roundTrip メソッドなどの間で伝達するために使われていました。

技術的詳細

このコミットの主要な技術的変更点は、net/http パッケージ内で発生する特定のネットワークエラー(特にタイムアウトエラーとコネクションクローズエラー)を、新しく定義された内部構造体 httpError を介して net.Error インターフェースを満たすようにしたことです。

  1. httpError 構造体の導入: net/http/transport.gohttpError という新しい内部構造体が定義されました。

    type httpError struct {
        err     string
        timeout bool
    }
    

    この構造体は、エラーメッセージ (err フィールド) と、そのエラーがタイムアウトによるものかどうかを示すブール値 (timeout フィールド) を保持します。

  2. net.Error インターフェースの実装: httpError 構造体は、Error() stringTimeout() boolTemporary() bool の3つのメソッドを実装することで、net.Error インターフェースを満たします。

    func (e *httpError) Error() string   { return e.err }
    func (e *httpError) Timeout() bool   { return e.timeout }
    func (e *httpError) Temporary() bool { return true } // HTTPトランスポートのエラーは一時的と見なされる
    

    Temporary() メソッドが常に true を返すのは、HTTPトランスポート層で発生するエラー(例えば、コネクションクローズやタイムアウト)は、通常、一時的なネットワークの問題やサーバーの負荷によるものであり、再試行によって解決する可能性があるためです。

  3. 事前定義されたエラー変数の導入: タイムアウトエラーとコネクションクローズエラーを表すために、errTimeouterrClosed という2つのグローバルな error 変数が導入されました。これらは httpError 型のインスタンスとして初期化されます。

    var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true}
    var errClosed error = &httpError{err: "net/http: transport closed before response was received"}
    

    これにより、エラー発生時に毎回新しいエラーオブジェクトを作成するオーバーヘッドが削減され、エラーの比較がポインタ比較で行えるようになります(ただし、このコミットの主な目的はインターフェースの実装です)。

  4. エラー発生箇所の変更: persistConn.roundTrip メソッド内で、以前は errors.New() を使って生成されていたエラーが、新しく定義された errClosed および errTimeout 変数に置き換えられました。

    • コネクションがクローズされた場合: re = responseAndError{err: errors.New("net/http: transport closed before response was received")} から re = responseAndError{err: errClosed} へ変更。
    • レスポンスヘッダのタイムアウトが発生した場合: re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")} から re = responseAndError{err: errTimeout} へ変更。
  5. テストケースの追加: net/http/transport_test.go に新しいテストケースが追加され、http.Client.Get から返されるエラーが url.Error にラップされ、その内部のエラーが net.Error インターフェースを満たし、かつ Timeout() メソッドが正しく true を返すことを検証しています。

    uerr, ok := err.(*url.Error)
    // ...
    nerr, ok := uerr.Err.(net.Error)
    // ...
    if !nerr.Timeout() {
        t.Errorf("want timeout error; got: %q", nerr)
        continue
    }
    

    このテストは、クライアントがエラーの型アサーションを通じてタイムアウトを検出できるようになったことを確認します。

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

src/pkg/net/http/transport.go

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -869,6 +869,18 @@ type writeRequest struct {
 	ch  chan<- error
 }
 
+type httpError struct {
+	err     string
+	timeout bool
+}
+
+func (e *httpError) Error() string   { return e.err }
+func (e *httpError) Timeout() bool   { return e.timeout }
+func (e *httpError) Temporary() bool { return true }
+
+var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true}
+var errClosed error = &httpError{err: "net/http: transport closed before response was received"}
+
 func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
 	pc.t.setReqConn(req.Request, pc)
 	pc.lk.Lock()
@@ -939,11 +951,11 @@ WaitResponse:
 		pconnDeadCh = nil                               // avoid spinning
 		failTicker = time.After(100 * time.Millisecond) // arbitrary time to wait for resc
 	case <-failTicker:
-		re = responseAndError{err: errors.New("net/http: transport closed before response was received")}
+		re = responseAndError{err: errClosed}
 		break WaitResponse
 	case <-respHeaderTimer:
 		pc.close()
-		re = responseAndError{err: errors.New("net/http: timeout awaiting response headers")}
+		re = responseAndError{err: errTimeout}
 		break WaitResponse
 	case re = <-resc:
 		break WaitResponse

src/pkg/net/http/transport_test.go

--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1237,6 +1237,20 @@ func TestTransportResponseHeaderTimeout(t *testing.T) {
 	for i, tt := range tests {
 		res, err := c.Get(ts.URL + tt.path)
 		if err != nil {
+			uerr, ok := err.(*url.Error)
+			if !ok {
+				t.Errorf("error is not an url.Error; got: %#v", err)
+				continue
+			}
+			nerr, ok := uerr.Err.(net.Error)
+			if !ok {
+				t.Errorf("error does not satisfy net.Error interface; got: %#v", err)
+				continue
+			}
+			if !nerr.Timeout() {
+				t.Errorf("want timeout error; got: %q", nerr)
+				continue
+			}
 			if strings.Contains(err.Error(), tt.wantErr) {
 				continue
 			}

コアとなるコードの解説

src/pkg/net/http/transport.go の変更点

  1. httpError 型の定義と net.Error インターフェースの実装: このセクションで最も重要なのは、httpError 構造体が定義され、それが Error() stringTimeout() boolTemporary() bool の各メソッドを実装することで、net.Error インターフェースを明示的に満たすようになった点です。これにより、net/http パッケージから返される特定のエラーが、ネットワークエラーとしての特性(タイムアウトかどうか、一時的かどうか)をプログラム的に公開できるようになりました。

  2. 事前定義されたエラー変数の導入: errTimeouterrClosed という2つの error 型の変数が導入されました。これらはそれぞれ、HTTPレスポンスヘッダのタイムアウトと、レスポンス受信前にトランスポートがクローズされた場合のエラーを表します。これらの変数は httpError 型のインスタンスとして初期化され、timeout フィールドが適切に設定されています。これにより、エラー発生時に毎回新しいエラーオブジェクトを生成するのではなく、既存のインスタンスを再利用できるようになります。

  3. エラー生成箇所の変更: persistConn.roundTrip メソッド内の WaitResponse ラベルが付いた select ステートメント内で、以前は errors.New() を使って文字列からエラーを生成していた箇所が、新しく定義された errClosed および errTimeout 変数に置き換えられました。

    • case <-failTicker: ブロックでは、トランスポートがクローズされた場合のエラーとして errClosed が使用されます。
    • case <-respHeaderTimer: ブロックでは、レスポンスヘッダのタイムアウトが発生した場合のエラーとして errTimeout が使用されます。 この変更により、net/http パッケージが返すエラーが net.Error インターフェースを満たすようになり、クライアント側でエラーの型アサーションを通じてタイムアウトを検出することが可能になります。

src/pkg/net/http/transport_test.go の変更点

  1. タイムアウトエラーの型アサーションテストの追加: TestTransportResponseHeaderTimeout 関数内のエラーハンドリングロジックが拡張されました。以前はエラーメッセージの文字列に strings.Contains を使ってタイムアウトを検出していましたが、このコミットにより、より堅牢な方法が導入されました。
    • まず、返された err*url.Error 型であるかを確認します。http.Client.Get などは、内部で発生したネットワークエラーを url.Error にラップして返すためです。
    • 次に、url.Error の内部エラー (uerr.Err) が net.Error インターフェースを満たすかを確認します。これがこのコミットの核心部分です。
    • 最後に、net.Error インターフェースの Timeout() メソッドを呼び出し、それが true を返すことを検証します。これにより、エラーが実際にタイムアウトによるものであることをプログラム的に確認できます。

このテストの追加は、変更が意図通りに機能し、クライアントがエラー文字列に依存することなくタイムアウトを検出できるようになったことを保証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: net パッケージ, net/http パッケージ
  • Go言語のエラーハンドリングに関する一般的な情報源
  • GitHubのGoリポジトリのIssueトラッカー
  • Go Code Reviewのウェブサイト