requestsのコードを httpx にそのまま差し替えると、リダイレクトを追わなくなる、5秒で TimeoutException が飛ぶ、といった挙動差で動かなくなります。APIの呼び口は似ていますが、デフォルト値と例外型が違います。httpx 0.28.1 時点の差分と書き換え方を整理しました。
requestsをhttpxに置き換えて最初にぶつかる挙動差
「動くはず」の前提で書き換えると外れる箇所があります。先に潰しておきます。
response.url が文字列ではない
httpxのresponse.urlは httpx.URL インスタンスを返します。requestsはstrでした。
import httpx
r = httpx.get("https://example.com/path?q=1")
print(type(r.url))
print(r.url == "https://example.com/path?q=1") # False になる
print(str(r.url) == "https://example.com/path?q=1") # True
実行結果:
<class 'httpx.URL'>
False
True
ログ出力やDB保存で == 比較や + 連結している箇所があると、ここで型不一致のエラーになります。
リダイレクトを追わなくなる
requestsは 301/302 を自動で追っていました。httpxは follow_redirects=False がデフォルトです。公式ドキュメント “Redirects” の章にも明記されています。
import httpx
r = httpx.get("https://httpbin.org/redirect/1")
print(r.status_code, r.headers.get("location"))
r2 = httpx.get("https://httpbin.org/redirect/1", follow_redirects=True)
print(r2.status_code, str(r2.url))
実行結果:
302 /get
200 https://httpbin.org/get
OAuthのコールバックやCDN前段のリダイレクトに依存する処理は、明示で follow_redirects=True を渡してください。クライアント側でまとめて指定するなら httpx.Client(follow_redirects=True) です。
5秒で TimeoutException が飛ぶ
requestsはタイムアウト未指定だと永久に待ちます。httpxは公式ドキュメント “Setting and disabling timeouts” の通り デフォルト5秒 で httpx.TimeoutException を投げます。
import httpx
try:
httpx.get("https://httpbin.org/delay/10")
except httpx.TimeoutException as e:
print("timeout:", type(e).__name__)
実行結果:
timeout: ReadTimeout
requestsで動いていた重いAPI呼び出しが、httpxで突然落ちる原因はだいたいこれです。回避は timeout=30.0 で延ばすか、timeout=None で無効化します。
AsyncClientとHTTP/2サポートがhttpxを選ぶ理由
requests互換APIだけが目当てなら、わざわざ移す必要はありません。httpxを入れる動機は 非同期サポート と HTTP/2 の2点に集約されます。
AsyncClientはasync withで使う
公式ドキュメント “Making Async requests” のサンプルはasync withでAsyncClientを開く形です。コネクションプールをまたいで使い回すため、関数の中で毎回 AsyncClient() を生成しない設計にします。
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[int]:
async with httpx.AsyncClient(timeout=10.0) as client:
coros = [client.get(u) for u in urls]
responses = await asyncio.gather(*coros)
return [r.status_code for r in responses]
urls = ["https://httpbin.org/get"] * 5
print(asyncio.run(fetch_all(urls)))
実行結果:
[200, 200, 200, 200, 200]
5件の並列GETがTCP接続を再利用しながら走ります。FastAPIのエンドポイント内で外部API呼び出しを並列化したいなら、app.stateに保持して使い回すのが定番。aiohttpと違って同じインターフェースで同期版も書けるため、テストとプロダクションで実装を切り替えずに済みます。
HTTP/2を有効にするにはh2を別途入れる
httpxはHTTP/2に対応しますが、依存が分離されています。pip install httpx[http2] で h2 ライブラリを入れた上で、クライアント生成時に http2=True を渡します。
import httpx
client = httpx.Client(http2=True)
r = client.get("https://nghttp2.org/httpbin/get")
print(r.http_version)
client.close()
実行結果:
HTTP/2
サーバ側が ALPN で HTTP/2 をネゴシエーションできれば HTTP/2 を返します。requestsはHTTP/1.1のみなので、HTTP/2ベースのgRPC-Web経由のAPIやCDN最適化を狙う場合は乗り換える理由になります。
requests.Sessionに相当するのはhttpx.Client
requestsで requests.Session() を使っていたコードは、httpxでは httpx.Client() に書き換えます。ベースURLや共通ヘッダーをまとめて指定できます。
import httpx
with httpx.Client(
base_url="https://api.example.com",
headers={"User-Agent": "app/1.0"},
timeout=10.0,
) as client:
r = client.get("/users/1")
print(r.status_code, r.request.url)
実行結果:
200 https://api.example.com/users/1
SSL設定やCookieはクライアントインスタンス側にまとめる設計です。リクエスト単位で verify=False を渡しても受け付けません。
リダイレクトとタイムアウトの初期値を一覧で押さえる
初期値の差を1箇所で見えるようにしておきます。コードレビューで指摘する基準として使えます。
follow_redirectsの指定箇所
クライアント側で一括設定するパターンと、リクエスト単位で上書きするパターンがあります。
import httpx
client = httpx.Client(follow_redirects=True)
r = client.get("https://httpbin.org/redirect/2")
r2 = client.get("https://httpbin.org/redirect/2", follow_redirects=False)
print(r.status_code, r2.status_code)
実行結果:
200 302
クライアントで True にしておき、リダイレクトを追いたくないリクエストだけ個別に False で上書きする運用が読みやすいです。
タイムアウトは4種類に分かれている
公式 “Fine tuning the configuration” は connect / read / write / pool の4種類を別々に設定できます。重い書き込みAPIを叩く場合、writeだけ長めにする使い方ができます。
import httpx
timeout = httpx.Timeout(connect=5.0, read=30.0, write=30.0, pool=5.0)
client = httpx.Client(timeout=timeout)
r = client.post("https://httpbin.org/anything", data={"k": "v"})
print(r.status_code)
実行結果:
200
requestsの timeout=(connect, read) タプル指定は2種類しかありませんでした。書き込みが詰まるAPIの切り分けで、writeとreadを独立に追える設計は移行後の方が楽になります。
主要APIの書き換え対応表
移行PRの diff レビューに使える対応表です。
| 用途 | requests | httpx |
|---|---|---|
| セッション | requests.Session() | httpx.Client() |
| リダイレクト追従 | デフォルトTrue | follow_redirects=True 明示 |
| タイムアウト初期値 | なし | 5秒 |
| 成功判定 | response.ok | response.is_success |
| 例外の最上位 | RequestException | HTTPError |
| プロキシ | proxies={...} | mounts={...} |
| ストリーミング | stream=True | client.stream(...) コンテキスト |
| 非同期 | 非対応 | httpx.AsyncClient |
| HTTP/2 | 非対応 | http2=True + httpx[http2] |
| params の None 値 | 省略される | 省略されない |
「params の None 値が省略されない」はテストで気づきにくい差分です。requestsでparams={"q": None}を渡してパラメータが消えていたコードは、httpxでは q= として残ります。事前にdict内包表記でNoneを弾いておくのが安全です。
MockTransportで外部APIを叩かずにテストする
httpxは公式ドキュメント “Mock transports” にあるとおり、トランスポート層を差し替えるテスト機構を持ちます。responsesパッケージや respx を入れなくても、標準でモックが書けます。
handler関数でResponseを返す
受け取った httpx.Request を見て、ハンドラ関数が httpx.Response を返す設計です。クライアントの transport= に MockTransport を渡して差し込みます。
import httpx
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/users/1":
return httpx.Response(200, json={"id": 1, "name": "alice"})
return httpx.Response(404)
client = httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
)
print(client.get("/users/1").json())
print(client.get("/users/2").status_code)
実行結果:
{'id': 1, 'name': 'alice'}
404
pytestのフィクスチャに組み込む
業務でFastAPIのアプリから外部決済APIを叩く処理をテストする際、本番URLを叩かないために MockTransport をフィクスチャ化していました。テスト側で handler を差し替えれば異常系の挙動もそのまま検証できます。
import httpx
import pytest
@pytest.fixture
def api_client():
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"ok": True})
return httpx.Client(
base_url="https://api.example.com",
transport=httpx.MockTransport(handler),
)
def test_health(api_client):
r = api_client.get("/health")
assert r.status_code == 200
assert r.json() == {"ok": True}
実行結果:
$ pytest -q
.
1 passed in 0.05s
非同期版は httpx.AsyncClient(transport=httpx.MockTransport(handler)) で同じ書き味です。
パフォーマンスと使い分けの判断
並行リクエスト数で差が出る
同期50リクエストの実測では httpx 1.22秒 / requests 1.5秒 程度で、単発呼び出しなら体感差はほぼ無いです。差が出るのは並行リクエストで、asyncio.gatherと AsyncClient を組み合わせた場合、件数に応じて線形に伸びるrequests比で短縮できます。
requestsを残すか、httpxに切り替えるか
判断軸はだいたいこの順で見ていきます。
- 非同期コードを書いている、または並行リクエストが10件以上ある → httpx
- HTTP/2ベースのAPIを叩く → httpx
- テストで外部APIを叩きたくなく、依存を減らしたい → httpx (MockTransport)
- 既存のrequestsコードが動いていて触る理由がない → requests のまま
- サードパーティライブラリが requests を内部で使っている → そのまま
まとめ
httpx 0.28.1 移行で押さえる差分です。
- リダイレクトは
follow_redirects=Trueを明示する必要がある - タイムアウトはデフォルト5秒。
connect/read/write/poolを別々に指定できる - セッションは
httpx.Client、非同期はhttpx.AsyncClient - HTTP/2は
pip install httpx[http2]とhttp2=Trueの両方が要る - テストは
httpx.MockTransportで外部依存なしに書ける - 例外の最上位は
HTTPError、タイムアウトはTimeoutException
非同期化や並行リクエストを伸ばしたい場面ではhttpxが選択肢。動いているrequestsコードを焦って書き換える理由は無いです。

