nikkie-ftnextの日記

イベントレポートや読書メモを発信

httpxのClientのtimeoutの設定について、たぶん完全に理解したと思います!(きめ細かく設定できる!)

はじめに

夏服...😭 nikkieです1

最近の学びをアウトプット!
httpx(心の中ではふててぺっくす読み😉)についてです。

目次

HTTPクライアントライブラリ httpx

PythonのHTTPクライアントライブラリには、標準ライブラリのurllibや、デファクトスタンダード感のあるrequestsがありますが、私が注目しているのがhttpx

「Features」のところにあるのですが、

HTTPX builds on the well-established usability of requests

意訳:HTTPXはrequestsのよく確立された使いやすさを活かし

つまりrequestsと同様の使い方ができ、その上requestsにはない非同期リクエストまでサポートしています!2

  • A broadly requests-compatible API.
    • 広くrequests互換なAPI
  • Standard synchronous interface, but with async support if you need it.
    • 標準的な同期インターフェース、しかし必要であれば非同期サポート

httpx利用シーンで遭遇したエラー

requestsにはSessionがありますが、これと同じものがhttpxではClient3

これを使い、素振りを兼ねて(また将来非同期で書き換えられたらいいなという想いも心に秘めて)、Web APIにリクエストを送るコードを書いていました。

このWeb APIからはページネーションを指定してデータを取得していたのですが、容量が大きいページがたまにあり、そこで処理が落ちるようでした。
Tracebackで目にしていたのはReadTimeout

対処:鍵はClientのtimeout

httpxを使わずにcurlコマンドを使うと時間がかかるものの取得できることを確認。
時間がかかるという点からドキュメントのtimeoutの項目を参照し、設定したことで解消しました(安定してデータを取り切れた!🙌)

httpxのドキュメントより timeout

Quickstartの「Timeouts」には以下のようにあります。
https://www.python-httpx.org/quickstart/#timeouts

The default timeout for network inactivity is five seconds.

ネットワークの不活性によるデフォルトのタイムアウトは5秒です

httpxではtimeoutをきめ細かく設定できるようで、Advancedのドキュメントに案内されます。
https://www.python-httpx.org/advanced/#fine-tuning-the-configuration

There are four different types of timeouts that may occur. These are connect, read, write, and pool timeouts.

4つの異なる書類のタイムアウトが発生するかもしれない。
connect, read, write, poolのタイムアウト

目にしていたReadTimeoutは、4種類のうちのreadのtimeoutということですね。

The read timeout specifies the maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body).
If HTTPX is unable to receive data within this time frame, a ReadTimeout exception is raised.

read timeoutは、データの塊を受け取るまでの最大の待ち時間を指定する(例:レスポンスボディの塊)
この時間枠内にHTTPXがデータを受け取れなかった場合、ReadTimeout例外が送出される

httpxのソースコード中のコメントより

read timeoutの設定にはソースコード中のコメントが特に助けになりました。
例が豊富で曖昧さがなかったのです。

https://github.com/encode/httpx/blob/0.24.0/httpx/_config.py#L195-L207

class Timeout:
    """
    Timeout configuration.

    **Usage**:

    Timeout(None)               # No timeouts.
    Timeout(5.0)                # 5s timeout on all operations.
    Timeout(None, connect=5.0)  # 5s timeout on connect, no other timeouts.
    Timeout(5.0, connect=10.0)  # 10s timeout on connect. 5s timeout elsewhere.
    Timeout(5.0, pool=None)     # No timeout on acquiring connection from pool.
                                # 5s timeout elsewhere.
    """

この中で注目したのがTimeout(5.0, connect=10.0)という例。

10s timeout on connect. 5s timeout elsewhere.

4つのうちのconnectのタイムアウトは10秒。残り3つ(read, write, pool)のタイムアウトは通して5秒という理解です。

curlで取得したときにかかった時間を元に、Timeout(5.0, read=10.0)のようにread timeoutだけ個別に設定することで、エラーは再現しなくなりました!

Timeout(5.0)という例にありますが、これは4つ(connect, read, write, pool)全部通して5秒のタイムアウトで、Quickstartのドキュメントで言われていたやつですね4

実際httpx.Clienttimeout引数のデフォルト値を確認すると、

再現実験

簡単に実験してみましょう。

  • Python 3.10.9
  • httpx 0.24.0
  • Flask 2.3.2

Flaskのアプリ(app.py)を用意します。

import time

from flask import Flask

app = Flask(__name__)


@app.route("/wait/<int:second>")
def wait_then_return(second):
    time.sleep(second)
    return "Hello, World"

URLパラメタで指定された秒数待つエンドポイントを実装しました。

flask --app app runで動かし、別のターミナルから以下のスクリプトを実行します。

import sys

import httpx

with httpx.Client() as client:
    print("----- Request start -----")
    r = client.get(f"http://127.0.0.1:5000/wait/{sys.argv[1]}")
    print("----- Request end -----")
    print(r)
% python client.py 2
----- Request start -----
----- Request end -----
<Response [200 OK]>

デフォルトのTimeout(timeout=5.0)を超える待ち時間を指定すると、ReadTimeoutスクリプトは落ちます(「Request end」)がありません。

% python client.py 6
----- Request start -----
Traceback (most recent call last):

(... 省略 ...)

httpx.ReadTimeout: timed out

read timeoutを10秒まで伸ばしてみましょう。

import sys

import httpx

-with httpx.Client() as client:
+with httpx.Client(timeout=httpx.Timeout(5.0, read=10.0)) as client:
    print("----- Request start -----")
    r = client.get(f"http://127.0.0.1:5000/wait/{sys.argv[1]}")
    print("----- Request end -----")
    print(r)
% python client.py 6
----- Request start -----
----- Request end -----
<Response [200 OK]>

6秒待つリクエストで落ちなくなりました!

終わりに

httpxClienttimeoutの設定について理解したことのアウトプットでした。

  • httpxではtimeoutには4種類ある
    • connect, read, write, pool
  • Timeout(5.0):4種類のtimeoutを通して5秒でtimeoutという設定
    • デフォルト設定
  • Timeout(5.0, read=10.0)
    • readのtimeoutだけ10秒、ほか3つは通して5秒という指定

引き続き手になじませていき、非同期でも使いこなしたいですね


  1. ここだけ切り取るとなんだかやべーやつですが、こころちゃんが制服で学校に行っているということがとても心を温めてくれるのです。気になる方はU-NEXTで配信をどうぞ!
  2. 上位互換ってことになるんですかね?(requests詳しくないので細かいところが分からず...)
  3. If you are coming from Requests, httpx.Client() is what you can use instead of requests.Session(). https://www.python-httpx.org/advanced/#client-instances のHint
  4. 通してってどう実装しているんだろう? 時間測ってるやつがいるのかな