httpx2とは—Pydanticのhttpxフォークと移行で壊れる箇所

Python 3.14.6 のクリーンな venv に pip install httpx2 を打つと、こう入ります。

anyio==4.14.2
h11==0.16.0
httpcore2==2.12.0
httpx2==2.12.0
idna==3.19
truststore==0.10.4

certifi がいません。同じ手順で httpx を入れた venv には certifi==2026.7.22httpcore==1.0.9 が並びます。名前を付け替えただけのフォークではありません。

目次

httpx2 は httpx 0.28.1 のフォーク

Pydantic が保守を引き受けた

httpx2 は Pydantic チームが公開している httpx のフォークです。CHANGELOG の 2.0.0b1 の項に、httpx 0.28.1 のコミット b5addb6 から分岐したと明記されています。理由は README に書いてあります。

With HTTPX itself seeing limited activity recently, Pydantic is picking up stewardship under the HTTPX2 name so that users have a reliably maintained path forward – including timely security updates for a library that sits in the critical path of so many production systems.

「httpx 自体の活動が最近限定的になっているので、Pydantic が HTTPX2 という名前で保守を引き継ぐ」。表現は抑えめです。一方、これを採用した側はもっと踏み込みます。anthropic SDK の MIGRATION.md にある “The SDK is built on httpx2” セクションは、httpx を which is no longer actively maintained と書き切っています。

httpx 本体は止まっていない

PyPI のリリース履歴を当たると、httpx の安定版は 0.28.1(2024-12-06) のまま 1年8か月動いていません。ただし開発版は生きていて、1.0.dev5 が 2026-08-21 に出ています。「保守されていない」は言い過ぎで、実態は「安定版が長期間出ていない」です。ただ、移行するかどうかは httpx 側の事情ではもう決まりません。

SDK 側がすでに httpx2 を要求している

  • anthropic 1.0.0(2026-08-20)は httpx2<3,>=2.0.0 を要求する
  • openai 3.3.1httpx2<3,>=2.7.0 を要求する
  • starlette 1.6.0full extra に httpx2>=2.0.0httpx>=0.27.0,<0.29.0 の両方を並べている
  • fastapi 0.141.1 はまだ httpx>=0.23.0,<1.0.0 のみ

anthropic か openai の SDK を上げた瞬間、httpx2 は依存としてついてきます。自分で選ぶ前に環境へ入る、という順序で来る移行です。httpx そのものの使い方はPython httpxとrequestsの違い:移行で変わる仕様と書き換え方にまとめてあります。

書き換え自体は import 1行

公式の移行ドキュメントは “In a Hurry?” というセクションで、依存を差し替えて import httpximport httpx2 に変えるだけ、と書いています。差分をさらに小さくするなら別名で受けます。

# before
import httpx

r = httpx.get("https://www.example.org/")

# after
import httpx2 as httpx

r = httpx.get("https://www.example.org/")

公開 API は変わっていないので、ClientAsyncClientResponse もそのまま通ります。しかも httpxhttpx2 は import 名が違う別パッケージなので、同じ環境に共存できます。ドキュメントの “You Can Have Both Installed” が扱っているのがこの点で、依存ツリーの全員が移行し終わるのを待つ必要はありません。残りを洗い出すコマンドも2つ挙げられています。

grep -rn "import httpx\b" src/
pipdeptree --reverse --packages httpx

オブジェクトはパッケージの境界を越えない

1行置換で終わったつもりになると、ここが残ります。共存できることと、混ぜて使えることは別です。

httpx.Timeout を httpx2.Client に渡すと黙って壊れる

アンチパターンから。httpx と httpx2 が両方入っている環境で、古いほうの Timeout を新しいほうの Client に渡します。

import httpx
import httpx2

c = httpx2.Client(timeout=httpx.Timeout(60.0, connect=5.0))
print("connect の型:", type(c.timeout.connect))
print("read の型   :", type(c.timeout.read))
connect の型: <class 'httpx.Timeout'>
read の型   : <class 'httpx.Timeout'>

例外は出ません。指定した 5.0 と 60.0 は消えています。httpx2 の Timeout.__init__isinstance(timeout, Timeout) で自分のクラスかどうかだけを見ます。httpx.Timeout はそこに一致せず、タプルでもないので、最後の分岐で 4つのフィールド全部にオブジェクトそのものが代入される。connect も read も write も pool も、数値ではなく httpx.Timeout インスタンスになります。

気づくのは、そのオブジェクトを表示しようとしたときです。

TypeError: cannot use 'httpx.Timeout' as a set element (unhashable type: 'Timeout')

正しく渡した場合と並べると差が見えます。

c = httpx2.Client(timeout=httpx2.Timeout(60.0, connect=5.0))
print(c.timeout)
print("connect の型:", type(c.timeout.connect).__name__)
Timeout(connect=5.0, read=60.0, write=60.0, pool=60.0)
connect の型: float

transport を混ぜると空メッセージの AssertionError

transport も同じ経路で壊れますが、落ち方が違います。

c = httpx2.Client(transport=httpx.HTTPTransport())   # ここは通る
c.get("https://httpbin.org/get", timeout=15.0)       # ここで落ちる
AssertionError: 

メッセージが空の AssertionError。Client を作った行ではなくリクエストを投げた行で上がるので、スタックトレースが原因から離れます。DI で transport を注入している構成だと、注入元まで戻るのに時間がかかります。

受け取る側がモジュールを決める

公式ドキュメントの “But objects don’t cross the boundary” は、この状況を一文に畳んでいます。

the package that receives the object decides which module you create it from.

自分のコードは httpx2 に寄せ、まだ httpx のままの依存には httpx のオブジェクトを渡す。例外も同じで、httpx のまま動く依存から飛んだ httpx.HTTPErrorexcept httpx2.HTTPError は捕まえません。

anthropic SDK はこの穴を構築時に塞いでいて、http_client= に旧 httpx.Client を渡すと TypeError を投げます。MIGRATION.md の付記は so this cannot fail silently。逆に言えば、そう作られていない受け口では Timeout のように黙って壊れます。

respx と pytest-httpx がモックを素通りする

テストは落ちません。モックが当たらないまま、通信が外へ出ていきます。respx 0.23.1 の依存は httpx>=0.25.0、pytest-httpx 0.36.2 は httpx==0.28.*。どちらも httpx をパッチして通信を横取りします。アプリが httpx2 を使い始めると、パッチした場所を誰も通らなくなります。

route.called が False のまま外へ出ていく

import httpx
import httpx2
import respx


@respx.mock
def test_mock_is_bypassed():
    route = respx.get("http://127.0.0.1:9/ping").mock(
        return_value=httpx.Response(200, json={"ok": True})
    )
    try:
        httpx2.get("http://127.0.0.1:9/ping", timeout=2.0)
    except Exception as e:
        print("実際に外へ出た:", type(e).__name__, str(e)[:60])
    print("route.called =", route.called)
    assert route.called
実際に外へ出た: ConnectError [Errno 61] Connection refused
route.called = False
FAILED test_silent.py::test_mock_is_bypassed - AssertionError: assert False

モックには当たらず、テストは 127.0.0.1:9 へ本当に接続しにいきました。ここは閉じたポートなので接続拒否で済みます。外部 API の URL をモックしていたら、CI から本番へリクエストが飛びます。

anthropic の MIGRATION.md は “Tracing, instrumentation and mocking libraries” セクションで、OpenTelemetry の HTTPXClientInstrumentor、Sentry の httpx integration、respx、pytest-httpx、vcrpy を名指しし、These libraries can silently fail, making it difficult to identify failure points と書いています。ログにも出ず、テストは通ったり落ちたりします。

alias_httpx() をプラグインとして先に走らせる

httpx2.alias_httpx() を呼ぶと、import httpx が httpx2 に、import httpcore が httpcore2 に、プロセス全体で解決するようになります。pytest では、respx やテストモジュールより先に走らせる必要があるので、conftest.py ではなくプラグインとして読み込みます。

# tests/_alias_httpx.py
import httpx2

httpx2.alias_httpx()
# pyproject.toml
[tool.pytest.ini_options]
addopts = "-p tests._alias_httpx"
pythonpath = ["."]

anthropic の移行ガイドはこの形を an early plugin is the least intrusive way to run it before respx / pytest-httpx and your test modules are imported と説明しています。上のテストをそのまま、プラグイン指定だけ足して回します。

$ python -m pytest test_silent.py -q -s -p _alias_httpx
route.called = True
1 passed in 0.01s

テストコードは1文字も変えていません。import httpx が httpx2 を指すようになったので、respx のパッチ先と httpx2 の実体が同じモジュールになり、モックが当たります。

alias_httpx() の2つのルールと、効かない範囲

呼ぶのが遅いと RuntimeError で止まる

docstring がルールを明示しています。

Must be called before anything imports httpx or httpcore. Calling it again is a no-op.

破るとこうなります。

import httpx                      # 先に読んでしまう
from httpx2 import alias_httpx

alias_httpx()
RuntimeError: httpx was already imported; call `alias_httpx()` before any `import httpx`.

すでに読み込んだモジュールは古いクラスへの参照を握っていて、alias はそれを書き換えられません。中途半端に効くくらいなら落とす、という設計です。同じ理由で、公式ドキュメントは手書きの sys.modules["httpx"] = httpx2 を警告しています。依存の中に from httpx.something import ... が1つでもあると、サブモジュールが httpx の名前でもう一度読み込まれ、クラス階層が2本に戻るからです。

alias でも書き換わらないもの

対象alias_httpx() 後の挙動
import httpx / import httpcorehttpx2 / httpcore2 に解決される
importlib.metadata.version("httpx")0.28.1 のまま。パッケージのメタデータを読むため
logging.getLogger("httpx")httpx2 のログは流れない。ロガー名は httpx2httpcore2.*
spawn で起きた子プロセスimport をやり直すので、各プロセスで呼び直しが要る

メタデータのほうを実際に確かめます。

import importlib.metadata as md

import httpx2

httpx2.alias_httpx()

import httpx

print(httpx.__name__, md.version("httpx"))
httpx2 0.28.1

モジュールは httpx2、バージョン文字列は 0.28.1。バージョンで分岐している互換コードを書いていると、ここで判断を誤ります。ロガーのほうも同じで、logging.getLogger("httpx").setLevel(logging.CRITICAL) で黙らせていたデバッグログは、alias 後は httpcore2.connection という名前で流れ続けます。

ライブラリからは呼ばない

docstring の Libraries should never call this. がもう1つのルールです。プロセス全体で import httpx の意味を変える操作なので、決めていいのはアプリケーションだけ。移行ドキュメントは Think of it as a bridge, not a destination と位置づけていて、依存が全部移り終わったら呼び出しごと消す前提になっています。

証明書の検証が certifi から OS の trust store へ

冒頭の certifi が消えていた件です。CHANGELOG の 2.3.0(June 1st, 2026)にこう入りました。

Use truststore instead of certifi for default SSL verification, loading the operating system’s trust store.

既定の SSLContext が別クラスになる

import httpx
import httpx2

print(type(httpx.create_ssl_context()))
print("certifi の CA 数:", len(httpx.create_ssl_context().get_ca_certs()))
print(type(httpx2.create_ssl_context()))
<class 'ssl.SSLContext'>
certifi の CA 数: 121
<class 'truststore._api.SSLContext'>

truststore の SSLContextget_ca_certs() を持っていますが、呼ぶと NotImplementedError を投げます。信頼済み CA を列挙して検査する監査スクリプトや、CA 数をアサートするテストは、httpx2 に移した時点で落ちます。

効く環境変数と、効かない環境変数

create_ssl_context() のソースを読むと、verify=True のときの優先順位は SSL_CERT_FILESSL_CERT_DIR、truststore の順です。この2つは httpx 0.28.1 と同じ扱いなので、社内 CA をここで通していた環境は影響を受けません。

SSL_CERT_FILE=/etc/ssl/cert.pem      → ssl.SSLContext(CA 128件)
REQUESTS_CA_BUNDLE=/etc/ssl/cert.pem → truststore._api.SSLContext(無視)

requests 由来の REQUESTS_CA_BUNDLE は httpx 0.28.1 も読んでいないので、移行で変わった点ではありません。ただし、certifi バンドルが消えて OS の trust store に切り替わるぶん、同じコードで検証結果が変わる余地は残ります。Docker の slim イメージのように OS 側の CA が薄い環境では、SSL_CERT_FILE を明示するほうが揺れません。動くけれど結果が変わる移行という点では、Python 3.15のUTF-8デフォルト化—cp932読み込みの移行対処法と同じ形です。

httpx-sse と httpx-ws を落とせる

with httpx2.Client(timeout=10.0) as c:
    with c.sse("http://127.0.0.1:8731/events") as es:
        for event in es:
            print(f"{time.time() - t0:.2f}s event={event.event!r} data={event.data!r}")

ローカルに立てた text/event-stream のサーバー相手の出力です。

0.02s event='tick' data='{"n": 1}'
0.29s event='tick' data='{"n": 2}'
0.54s event='tick' data='{"n": 3}'

SSE と WebSocket が本体に入った

client.sse() は CHANGELOG 2.5.0(June 25th, 2026)で入りました。イベントのバッファ上限は max_event_size で、既定は 1048576 バイト。WebSocket は 2.6.0(July 14th, 2026)で httpx-ws を取り込む形で入り、pip install 'httpx2[ws]' で wsproto が入って client.websocket() が使えます。同じ 2.6.0 で QUERY メソッドも追加されました。httpx 0.28.1 の Client には ssewebsocketquery のどれも存在しません。

移行前後の差分

項目httpx 0.28.1httpx2 2.12.0
import 名httpxhttpx2
下回りhttpcore 1.0.9httpcore2 2.12.0
証明書certifi の同梱バンドルOS の trust store(truststore 0.10.4)
User-Agentpython-httpx/0.28.1python-httpx2/2.12.0
ロガー名httpx / httpcore.*httpx2 / httpcore2.*
SSEhttpx-sse が別途要るclient.sse() が標準
WebSockethttpx-ws が別途要るhttpx2[ws]client.websocket()
最小 Python3.83.10

サーバー側のログを python-httpx/ で集計している、あるいはテストで User-Agent を突き合わせている場合は、そこも書き換え対象になります。

まとめ

  • import httpx2 as httpx でコード側の差分は1行に収まる。壊れるのは、httpx と httpx2 のオブジェクトを混ぜた箇所だけ
  • httpx.Timeouthttpx2.Client に渡しても例外は出ず、connect と read の値が消える。transport を混ぜるとリクエスト時に空メッセージの AssertionError になる
  • respx 0.23.1 と pytest-httpx 0.36.2 は httpx をパッチするため、httpx2 のリクエストは素通りする。addopts = "-p tests._alias_httpx"alias_httpx() を先に走らせれば、テストコードを変えずに当たるようになる
  • alias_httpx()importlib.metadata.version("httpx") とロガー名までは書き換えない。バージョン分岐とログ設定は手で直す

anthropic 1.0.0 と openai 3.3.1 がすでに httpx2 を要求している以上、AI 関連の SDK を使っているプロジェクトは選択の余地なく両方が入ります。先に pipdeptree --reverse --packages httpx を打って、どの依存が httpx を掴んだままかを把握しておくと移行の順番を決めやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次