読者です 読者をやめる 読者になる 読者になる

Keep It MECE

あったこと、気になったこと、知っておきたいこと

Python: requests Timeouts(タイムアウト)について調べた

Pythonのrequests.
人間が読みやすく、のポリシーが書いてあるあれです。

今回はその中のTimeoutについて調べてみました。

Timeoutを設定する

requestsのドキュメントには、普通のプロダクションコードならTimeoutを必ず書く必要があって、ない場合はハングアップするよ、と書いてあります。

You can tell Requests to stop waiting for a response after a given number of seconds with the timeout parameter. Nearly all production code should use this parameter in nearly all requests. Failure to do so can cause your program to hang indefinitely:

例えば、以下のようなコードで実現できます。

# hung upするかもしれないコード
requests.get('http://example.com')
# hung upしないコード
requests.get('http://example.com', timeout=10)

timeoutはfloatで指定してもいいようです(5.5など)
ちなみに、デフォルト値はNone(タイムアウトなし)です。
明示的にNoneを指定すると、デフォルト値としてふるまいます。

Timeoutエラーの種類

タイムアウトを設定した場合、例外が送出される場合があります。
内部的にリトライしている場合は、リトライ回数が超えた、という文言も一緒に出力されるかもしれません。

ConnectTimeout

下記の例は、リトライを設定した上で、タイムアウトを0.001秒に設定した例です。
送出される例外はConnectTimeoutです。

# code
requests.get("<HOST>", timeout=0.001)

# raises
requests.exceptions.ConnectTimeout:
 HTTPSConnectionPool(host='<HOST>', port=<PORT>):
Max retries exceeded with url: <URL>
 (Caused by
 ConnectTimeoutError(<requests.packages.urllib3.connection.VerifiedHTTPSConnection>,
'Connection to <HOST> timed out. (connect timeout=0.001)'))
ValueError

Timeoutは0未満の値を受け付けないようです。当たり前ですが。

# code
requests.get("<HOST>", timeout=-1)

# raises
ValueError: Attempted to set connect timeout to -1,
 but the timeout cannot be set to a value less than 0.

エラーには、less than 0はセットできないとあります。
この場合はValueErrorが送出されます。

ConnectionError

ではタイムアウトに0を設定した場合はどうなるのでしょうか。
0未満がだめということなら0は設定できるはず。

# code
requests.get('<HOST>', timeout=0)

# raises
requests.exceptions.ConnectionError: 
 HTTPSConnectionPool(host='<HOST>', port='<PORT'):
 Max retries exceeded with url: <URL>
 (Caused by 
NewConnectionError('<requests.packages.urllib3.connection.VerifiedHTTPSConnection>:
 Failed to establish a new connection:
  [Errno 36] Operation now in progress',))

今度はConnectionErrorが送出されています。ConnectErrorとConnectionError、とても良く似ています。

スタックトレースの最後まで見ると、0.001秒と設定した時と比べて、以下のような違いがあるのがわかります。

# timeout=0.001
ConnectTimeoutError
Connection to <HOST> timed out.

# timeout=0
NewConnectionError
Failed to establish a new connection:

そんなの当たり前じゃん、という感じなんですが、timeout=0.001の場合は、「繋ぎに行ったけどタイムアウトしたわ、ごめんね」となったのに対し、timeout=0の場合は、「繋ごうとしたらtimeout=0だったからコネクションすら確立できなかったわ、すまんな」ということなんだと思います。

なんでこんなことを調べたのか

タイムアウトする場合のテストを書いていて、

  • timeout=0.001くらいにとしておけばだいたい大丈夫か?
  • requests.Timeoutsのサンプルがそうなっている
  • でも、将来的にちょうはやい回線が出てきたときに、1ms以内でレスポンス返るようになることもあるんじゃ?
  • まあ、多分そんなことはないと思うけど。
  • (もしあった場合に)そしたら0.0000001とかにするんかな?
  • timeoutは負の値は無理だけど0はいけるらしい
  • じゃあ、0にすればいいんじゃね???

という経緯で、0にしてみたら微妙に違うエラーが出た…ということです。

デフォルト値がNoneなのが微妙

関数の引数にtimeoutを渡す場合、デフォルト値を設定したくなるんですが、Noneは意味がある(タイムアウトなし)ため、デフォルト値に悩みます。
デフォルト値を負にして、負の場合は無指定とみなすか、0にして0の場合は無指定とみなすか、とか色々考えているところです。
普通のユースケースなら0で十分な気もします(0の場合はテストができなくなりますが)

最後に

Timeoutを書き忘れると1時間とか止まるケースがあるようなので(実体験)皆様、気をつけてください。