Overview
HTTP のりクエスをしたときに遭遇する dial tcp $IP: i/o timeout
をハンドリングする方法について、そもそもこれがどうして発生するのか?と併せて調べた備忘録です。
http.httpError と net.Error
Go で API クライアントを実装する際に遭遇する Timeout エラーのパターンとして http package 内で発生するのか net package 内で発生するのかで2種類が存在(※) します。
この2つはそもそも発生するユースケースが違います。
http.httpError の方は Context deadline exeeded なので Context の生存期間が切れてそれ以上処理を継続できなくなったために発生するエラーです。一方で net.Error は i/o timeout なのでコネクションの Timeout に起因するエラーになります。
net.Error の方が一段深いレイヤー(net.Conn のレイヤー)で発生したエラーをハンドリングするエラーになります。
※ net.Error は Interface なので net package 内のその他定義されてるエラーも必要な interface を実装していれば net.Error
として振る舞うことができます。
net.Error で発生する Timeout のハンドリング
net.Error は interface になっている のでこの interface を実装してるエラーであれば Timeout() を call して Timeout かどうかをハンドリングできます。
そのため以下のように net.Error
でキャストします。
resp, err := http.DefaultClient.Do(r) if err != nil { if ne, ok := err.(net.Error); ok && ne.Timeout() { // Timeout 時の何かしらの処理 } } return nil, err }
余談: 実装を追っかけてみた
そもそも最初の疑問は timeout のエラーにも関わらず、http package を探しても見つからなかったので、どういうケースで i/o timeout
がエラーが出力されるのかわからなかったので、調べることにしました。調べ方ですが、まずはどこでエラーが発火してるのか調べ、順々に呼び出し先を追っかけていきます。
エラー自体は net pacakge の timeoutError になります → https://cs.opensource.google/go/go/+/master:src/net/net.go;l=600
よく読むと timeoutError は net.Error を満たす interface を実装してるので net.Error の Timeout メソッドでハンドリングが可能になります → https://cs.opensource.google/go/go/+/master:src/net/net.go;l=598-602;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b
では実際どこで呼ばれることで i/o timeout
が発火するのでしょうか?この timoutError が発火する先を遡って探していきます。
timeoutErrror が定義されているのは https://cs.opensource.google/go/go/+/master:src/net/net.go;l=596;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b になります。
この errTimeout が呼ばれてるところを探すと dial.go の partialDeadline
で呼ばれてることがわかります → https://cs.opensource.google/go/go/+/master:src/net/dial.go;l=138;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1
さらにこの partialDeadline
を遡ります。そうすると同じく dial.go 内部の dialSerial
内で呼ばれてることがわかります → https://cs.opensource.google/go/go/+/master:src/net/dial.go;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1;l=523
この dialSerial
がどこで呼ばれてるか遡ると、その1つに dial.go 内の dialParallel
の内部で呼ばれてることがわかります → https://cs.opensource.google/go/go/+/master:src/net/dial.go;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1;l=449
最後にこの dialParallel
がどこで呼ばれてるのか調べると dial.go 内の DialContext
の中で呼ばれてることがわかります → https://cs.opensource.google/go/go/+/master:src/net/dial.go;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1;l=376
実はこの DialContext は http.DefaultClient を使うときに自動的に指定される DefaultTransport の内部で呼ばれている defaultTransportDialContext
ので呼ばれています。
defaultTransportDialContext
→ https://cs.opensource.google/go/go/+/master:src/net/http/transport_default_other.go;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1;l=15
DefaultTransport
→ https://cs.opensource.google/go/go/+/master:src/net/http/transport.go;l=43-54;drc=55590f3a2b89f001bcadf0df6eb2dde62618302b;bpv=1;bpt=1
これからわかるのは i/o timeout
は package が示す通り http のレイヤーではなく、一つ下の connection を管理するレイヤーで発火したエラーであることがわかります。Transport は DialContext で指定された値で接続されるので、timeout を発火させない場合はこの DialContext で指定する Timeout の値を増やせばいいことになります。
追記
Go1.19 で net.Error でキャストしなくて良くなる?
教えてもらいました。
Go 1.19でちょっと楽になりそうhttps://t.co/yVOdvQtTWY
— daisuzu (@dice_zu) 2022年6月12日
i/o timeout エラーをハンドリングする - emahiro/b.log https://t.co/OsnCBi56zw
実際に Go 1.19 のリリースノート(予定) を見ると、net package の箇所に以下のように記載してあります。
When a net package function or method returns an "I/O timeout" error, the error will now satisfy errors.Is(err, context.DeadlineExceeded). When a net package function returns an "operation was canceled" error, the error will now satisfy errors.Is(err, context.Canceled). These changes are intended to make it easier for code to test for cases in which a context cancellation or timeout causes a net package function or method to return an error, while preserving backward compatibility for error messages.
Go1.19 以降は net.Error でキャストして Timeout を呼ぶ必要はなく、context.DeadlineExceeded でハンドリングできるのは良いですね(http と意味合いが重なってしまうような気もしましたが)