[インデックス 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
インターフェースを満たすようにしたことです。
-
httpError
構造体の導入:net/http/transport.go
にhttpError
という新しい内部構造体が定義されました。type httpError struct { err string timeout bool }
この構造体は、エラーメッセージ (
err
フィールド) と、そのエラーがタイムアウトによるものかどうかを示すブール値 (timeout
フィールド) を保持します。 -
net.Error
インターフェースの実装:httpError
構造体は、Error() string
、Timeout() bool
、Temporary() 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トランスポート層で発生するエラー(例えば、コネクションクローズやタイムアウト)は、通常、一時的なネットワークの問題やサーバーの負荷によるものであり、再試行によって解決する可能性があるためです。 -
事前定義されたエラー変数の導入: タイムアウトエラーとコネクションクローズエラーを表すために、
errTimeout
とerrClosed
という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"}
これにより、エラー発生時に毎回新しいエラーオブジェクトを作成するオーバーヘッドが削減され、エラーの比較がポインタ比較で行えるようになります(ただし、このコミットの主な目的はインターフェースの実装です)。
-
エラー発生箇所の変更:
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}
へ変更。
- コネクションがクローズされた場合:
-
テストケースの追加:
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
の変更点
-
httpError
型の定義とnet.Error
インターフェースの実装: このセクションで最も重要なのは、httpError
構造体が定義され、それがError() string
、Timeout() bool
、Temporary() bool
の各メソッドを実装することで、net.Error
インターフェースを明示的に満たすようになった点です。これにより、net/http
パッケージから返される特定のエラーが、ネットワークエラーとしての特性(タイムアウトかどうか、一時的かどうか)をプログラム的に公開できるようになりました。 -
事前定義されたエラー変数の導入:
errTimeout
とerrClosed
という2つのerror
型の変数が導入されました。これらはそれぞれ、HTTPレスポンスヘッダのタイムアウトと、レスポンス受信前にトランスポートがクローズされた場合のエラーを表します。これらの変数はhttpError
型のインスタンスとして初期化され、timeout
フィールドが適切に設定されています。これにより、エラー発生時に毎回新しいエラーオブジェクトを生成するのではなく、既存のインスタンスを再利用できるようになります。 -
エラー生成箇所の変更:
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
の変更点
- タイムアウトエラーの型アサーションテストの追加:
TestTransportResponseHeaderTimeout
関数内のエラーハンドリングロジックが拡張されました。以前はエラーメッセージの文字列にstrings.Contains
を使ってタイムアウトを検出していましたが、このコミットにより、より堅牢な方法が導入されました。- まず、返された
err
が*url.Error
型であるかを確認します。http.Client.Get
などは、内部で発生したネットワークエラーをurl.Error
にラップして返すためです。 - 次に、
url.Error
の内部エラー (uerr.Err
) がnet.Error
インターフェースを満たすかを確認します。これがこのコミットの核心部分です。 - 最後に、
net.Error
インターフェースのTimeout()
メソッドを呼び出し、それがtrue
を返すことを検証します。これにより、エラーが実際にタイムアウトによるものであることをプログラム的に確認できます。
- まず、返された
このテストの追加は、変更が意図通りに機能し、クライアントがエラー文字列に依存することなくタイムアウトを検出できるようになったことを保証します。
関連リンク
- Go Issue #6185: net/http: allow clients to check for timeouts without relying on error substring matching
- Go CL 55470048: net/http: make responseAndError satisfy the net.Error interface
参考にした情報源リンク
- Go言語の公式ドキュメント:
net
パッケージ,net/http
パッケージ - Go言語のエラーハンドリングに関する一般的な情報源
- GitHubのGoリポジトリのIssueトラッカー
- Go Code Reviewのウェブサイト