次世代Python HTTPクライアント: httpx 詳細解説

Python

非同期、HTTP/2対応のモダンなライブラリを使いこなそう!

🚀 httpx とは?

httpx は、Python 3 用のモダンで高機能なHTTPクライアントライブラリです。有名な requests ライブラリにインスパイアされた使いやすいAPIを持ちながら、現代的なWeb開発の要求に応えるための強力な機能を備えています。

主な特徴として、非同期 (async/await) リクエストのサポートと、HTTP/1.1 および HTTP/2 の両プロトコルへの対応が挙げられます。これにより、大量のリクエストを効率的に処理したり、最新のWebサーバーとの通信を最適化したりすることが可能です。

requests を使ったことがある開発者であれば、比較的スムーズに httpx へ移行できます。それでいて、より高度な機能やパフォーマンスを求める場合に強力な選択肢となります。

httpx の主な利点 ✨

  • 非同期サポート: `asyncio`, `trio` といった非同期フレームワークと連携し、高効率なI/O処理を実現。
  • HTTP/2 サポート: より高速で効率的な通信プロトコルに対応(オプション)。
  • requests互換API: 既存の `requests` ユーザーにとって学習コストが低い。
  • 厳格なタイムアウト: 接続、読み取り、書き込み、プールなど、各所でタイムアウトを設定可能。
  • 型ヒント完全対応: 静的解析ツールとの相性が良く、開発効率とコード品質を向上。
  • WSGI/ASGI アプリケーションへの直接リクエスト: テストなどで便利。
  • コマンドラインクライアント: 簡単なリクエストならターミナルから直接実行可能(オプション)。

💻 インストール

httpx のインストールは pip を使って簡単に行えます。

pip install httpx

HTTP/2 サポートを利用したい場合は、追加の依存関係を含めてインストールします。Brotli圧縮のサポートも同様に追加できます。

# HTTP/2 サポートを追加
pip install httpx[http2]

# HTTP/3 サポートを追加 (httpcore の機能)
# httpx は内部で httpcore を使用しており、httpcore が HTTP/3 をサポートします
pip install httpx[http3]

# Brotli 圧縮サポートを追加
pip install httpx[brotli]

# 複数まとめてインストール
pip install httpx[http2,http3,brotli]

また、コマンドラインから httpx を使いたい場合は、cli オプションを追加します。

pip install httpx[cli]

注意: httpx は Python 3.8 以降が必要です。

⚙️ 基本的な使い方 (同期クライアント)

requests ライブラリと同様の感覚で、同期的なHTTPリクエストを簡単に送信できます。

GET リクエスト

最も基本的な GET リクエストの例です。

import httpx

# シンプルな GET リクエスト
response = httpx.get('https://httpbin.org/get')

# ステータスコードの確認
print(f"ステータスコード: {response.status_code}")

# ヘッダーの表示 (辞書ライクなオブジェクト)
print(f"Content-Type: {response.headers.get('content-type')}")

# レスポンスボディ (テキスト)
# print(f"レスポンスボディ:\n{response.text}")

# レスポンスボディ (JSON)
try:
    json_data = response.json()
    print("レスポンス JSON:")
    import json
    print(json.dumps(json_data, indent=2, ensure_ascii=False))
except httpx.ResponseNotRead:
    print("レスポンスボディはまだ読み込まれていません。")
except json.JSONDecodeError:
    print("レスポンスは有効な JSON ではありません。")
except Exception as e:
    print(f"エラーが発生しました: {e}")

# レスポンスボディ (バイナリ)
# print(f"レスポンスボディ (bytes): {response.content}")

POST リクエスト (JSONデータ)

JSON データを送信する場合は json 引数を使用します。

import httpx
import json

payload = {'name': 'Taro Yamada', 'age': 30}

# POST リクエスト (JSON)
response = httpx.post('https://httpbin.org/post', json=payload)

print(f"ステータスコード: {response.status_code}")
try:
    response_data = response.json()
    print("サーバーからのレスポンス JSON:")
    print(json.dumps(response_data, indent=2, ensure_ascii=False))
except Exception as e:
    print(f"JSON デコードエラー: {e}")

クエリパラメータとカスタムヘッダー

params 引数でクエリパラメータを、headers 引数でカスタムヘッダーを付与できます。

import httpx
import json

params = {'search': 'python httpx', 'page': 1}
headers = {'User-Agent': 'my-cool-app/1.0', 'Accept': 'application/json'}

response = httpx.get('https://httpbin.org/get', params=params, headers=headers)

print(f"ステータスコード: {response.status_code}")
try:
    print("サーバーからのレスポンス JSON:")
    print(json.dumps(response.json(), indent=2, ensure_ascii=False))
except Exception as e:
    print(f"JSON デコードエラー: {e}")

Client インスタンスの使用

複数のリクエストで設定(ヘッダー、Cookie、タイムアウトなど)を共有したい場合や、コネクションプーリングを活用したい場合は httpx.Client を使用します。with 文を使うことで、リソースが確実に解放されます。

import httpx
import json

# Client インスタンスを作成 (with 文で自動的にクローズ)
with httpx.Client(base_url='https://httpbin.org', headers={'X-Custom-Header': 'value'}) as client:
    # GET リクエスト (/get)
    response_get = client.get('/get', params={'param1': 'test'})
    print(f"GET ステータス: {response_get.status_code}")
    try:
        print("GET レスポンス:")
        print(json.dumps(response_get.json(), indent=2, ensure_ascii=False))
    except Exception as e:
        print(f"JSON デコードエラー: {e}")

    print("-" * 20)

    # POST リクエスト (/post)
    response_post = client.post('/post', json={'message': 'Hello from Client!'})
    print(f"POST ステータス: {response_post.status_code}")
    try:
        print("POST レスポンス:")
        print(json.dumps(response_post.json(), indent=2, ensure_ascii=False))
    except Exception as e:
        print(f"JSON デコードエラー: {e}")

httpx.Client()requests.Session() と同様の目的で使用され、コネクションの再利用や設定の共有に役立ちます。

⚡ 非同期クライアントの使い方 (Async)

httpx の真価が発揮されるのが非同期処理です。Python の asyncio などの非同期フレームワークと組み合わせることで、特に多数の I/O 待ちが発生するようなタスク(例: 大量の API リクエスト)を非常に効率的に実行できます。

非同期処理には asyncawait キーワード、そして httpx.AsyncClient を使用します。

基本的な非同期 GET リクエスト

import httpx
import asyncio
import json

async def fetch_data(url: str):
    # 非同期クライアントを生成
    async with httpx.AsyncClient() as client:
        print(f"リクエスト開始: {url}")
        # await を使って非同期にリクエストを実行
        response = await client.get(url)
        print(f"リクエスト完了: {url}, ステータス: {response.status_code}")
        try:
            return response.json()
        except Exception as e:
            print(f"JSON デコードエラー ({url}): {e}")
            return None

async def main():
    url1 = 'https://httpbin.org/get?source=async1'
    url2 = 'https://httpbin.org/delay/1' # 1秒待つAPI

    # asyncio.gather を使って複数の非同期タスクを並行実行
    results = await asyncio.gather(
        fetch_data(url1),
        fetch_data(url2)
    )

    print("\\n--- 取得結果 ---")
    for i, result in enumerate(results):
        if result:
            print(f"結果 {i+1}:")
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print(f"結果 {i+1}: 取得失敗")

# 非同期関数を実行
if __name__ == "__main__":
    asyncio.run(main())

上記の例では、fetch_data という非同期関数を定義し、asyncio.gather を使って2つのURLへのリクエストを同時に(並行して)開始します。/delay/1 エンドポイントは1秒待ってから応答しますが、その間にもう一方の /get リクエストは処理を進めることができます。これにより、逐次的にリクエストを送るよりも全体の処理時間を短縮できます。

非同期処理のポイント 💡

  • async def で非同期関数を定義します。
  • 時間のかかる I/O 処理(httpx のリクエストなど)の前には await を付けます。これにより、処理が完了するまで他のタスクに制御を移すことができます。
  • httpx.AsyncClient を非同期クライアントとして使用します。with 文 (async with) と組み合わせるのが一般的です。
  • 複数の非同期タスクを並行実行するには asyncio.gather などを使用します。
  • 非同期処理は、ネットワークI/OやファイルI/Oなど、CPUではなく待ち時間が多い処理で特に効果を発揮します。

🤔 httpx vs requests 比較

httpx は多くの点で requests と似ていますが、重要な違いもいくつかあります。どちらを選択するかは、プロジェクトの要件によって異なります。

機能 httpx requests 備考
API デザイン ほぼ requests 互換 広く使われているデファクトスタンダード httpxrequests からの移行が容易
非同期サポート ネイティブ対応 (async/await) 非対応 requests で非同期を実現するには別ライブラリや工夫が必要
HTTP/2 サポート 対応 (オプション) 非対応 httpxpip install httpx[http2] で有効化
HTTP/3 サポート 対応 (オプション, httpcore経由) 非対応 pip install httpx[http3] で有効化 (QUIC / UDPベース)
タイムアウト 詳細設定可能 (Connect, Read, Write, Pool) 基本的なタイムアウト設定のみ httpx はより厳格な制御が可能
WSGI/ASGI 直接リクエスト 対応 非対応 テストフレームワークとの連携に便利
コマンドラインクライアント あり (オプション) なし pip install httpx[cli] で利用可能
開発状況 活発 安定・メンテナンスモード httpx は新しい機能が積極的に追加されている
依存ライブラリ httpcore, h11, sniffio, certifi, idna 等 urllib3, chardet, certifi, idna 等 基盤となるライブラリが異なる

どちらを選ぶべきか?

  • 単純なスクリプトや、同期処理のみで十分な場合: requests が依然としてシンプルで良い選択肢です。ドキュメントやコミュニティサポートも豊富です。
  • 非同期処理が必要な場合(Webフレームワーク、大量のAPIアクセスなど): httpx が最適です。
  • HTTP/2 や HTTP/3 の機能を利用したい場合: httpx を選択します。
  • モダンなPython機能(型ヒントなど)を最大限活用したい場合: httpx の方がより現代的な設計になっています。
  • 新しいライブラリを試してみたい、より高機能・高性能なクライアントが必要な場合: httpx を検討する価値があります。

2025年現在、多くの新しいプロジェクト、特に非同期処理を前提とする FastAPI や Starlette などのフレームワークと組み合わせる際には、httpx が第一候補となるケースが増えています。

🛠️ 高度な機能

httpx は基本的なリクエスト以外にも、多くの高度な機能を提供します。

HTTP/2 と HTTP/3 サポート 🚀

前述の通り、httpx は HTTP/2 と HTTP/3 をサポートします。これにより、特に多重化(一つの接続で複数のリクエスト/レスポンスをやり取りする)によるパフォーマンス向上が期待できます。

利用するには、まず対応する依存ライブラリをインストールします。

# HTTP/2 の場合
pip install httpx[http2]

# HTTP/3 の場合
pip install httpx[http3]

そして、Client または AsyncClient を初期化する際に http2=True を指定します。HTTP/3 は、サーバーが対応していれば自動的にネゴシエーションされる場合があります(httpcore の実装に依存)。

import httpx
import asyncio

async def check_http_version(url: str):
    # http2=True を指定して HTTP/2 を優先的に試みる
    # サーバーが HTTP/3 に対応していれば、httpcore がそれを選択する可能性もある
    async with httpx.AsyncClient(http2=True, verify=False) as client: # verify=False は自己署名証明書などの場合に注意して使用
        try:
            response = await client.get(url)
            print(f"{url} - 使用されたプロトコル: {response.http_version}")
            # response.raise_for_status() # 必要に応じてステータスチェック
        except httpx.RequestError as exc:
            print(f"{url} へのリクエスト中にエラーが発生しました: {exc.__class__.__name__}")
        except Exception as e:
            print(f"{url} で予期せぬエラー: {e}")

async def main():
    # HTTP/2 や HTTP/3 をサポートしている可能性のあるサイト
    # 注意: サーバー側の設定やネットワーク環境により結果は異なります
    urls = [
        "https://www.google.com",
        "https://httpbin.org/headers", # HTTP/1.1 のみの場合が多い
        "https://cloudflare.com",
        # "https://localhost:8443" # ローカルでテストサーバーを立てる場合など
    ]
    tasks = [check_http_version(url) for url in urls]
    await asyncio.gather(*tasks)

if __name__ == "__main__":
    # 注意: 自己署名証明書などを使用しているサーバーの場合、SSL検証エラーが発生することがあります。
    # その場合は `verify=False` を指定するか、適切な証明書設定が必要です。
    # 本番環境での `verify=False` の使用はセキュリティリスクを伴います。
    asyncio.run(main())

HTTP/2 や HTTP/3 の利用はサーバー側の対応が必要です。また、HTTP/3 は UDP 上の QUIC プロトコルを使用するため、ネットワーク環境(ファイアウォールなど)によっては通信できない場合があります。HTTP/3 はデフォルトで TLSv1.3 による暗号化が必須です。

タイムアウト設定 ⏱️

httpx では、きめ細やかなタイムアウト設定が可能です。httpx.Timeout オブジェクトを使って指定します。

  • connect: サーバーへの接続確立までのタイムアウト(秒)。
  • read: サーバーからデータのチャンクを読み取る間のタイムアウト(秒)。
  • write: サーバーにデータのチャンクを書き込む間のタイムアウト(秒)。
  • pool: コネクションプールから接続を取得するまでのタイムアウト(秒)。

デフォルトではすべて5秒に設定されています。

import httpx

# 接続タイムアウトを 2秒、読み取りタイムアウトを 10秒に設定
timeout_config = httpx.Timeout(2.0, read=10.0)

try:
    with httpx.Client(timeout=timeout_config) as client:
        # このリクエストは最大10秒待機する
        response = client.get('https://httpbin.org/delay/5') # 5秒遅延するエンドポイント
        print("タイムアウトせずに成功しました。")
        # response = client.get('https://httpbin.org/delay/15') # これは read タイムアウトになるはず
        # print("タイムアウトせずに成功しました。")

except httpx.ReadTimeout:
    print("読み取りタイムアウトが発生しました。")
except httpx.ConnectTimeout:
    print("接続タイムアウトが発生しました。")
except httpx.TimeoutException as e:
    print(f"その他のタイムアウト関連エラー: {e}")
except httpx.RequestError as e:
    print(f"リクエストエラー: {e}")

リダイレクト制御

デフォルトでは、httpx はリダイレクトに追従します (最大30回)。follow_redirects=False を設定することで、リダイレクトを無効にできます。

import httpx

url = 'https://httpbin.org/redirect-to?url=https://www.example.com'

# リダイレクトに追従する (デフォルト)
response_follow = httpx.get(url)
print(f"追従あり - 最終URL: {response_follow.url}")
print(f"追従あり - ステータス: {response_follow.status_code}")
print(f"追従あり - リダイレクト履歴: {response_follow.history}")

print("\\n" + "-"*20 + "\\n")

# リダイレクトに追従しない
response_nofollow = httpx.get(url, follow_redirects=False)
print(f"追従なし - URL: {response_nofollow.url}")
print(f"追従なし - ステータス: {response_nofollow.status_code}") # 3xx 系になるはず
print(f"追従なし - Locationヘッダ: {response_nofollow.headers.get('location')}")
print(f"追従なし - リダイレクト履歴: {response_nofollow.history}") # 空になるはず

認証 (Authentication) 🔑

Basic 認証や Digest 認証などをサポートしています。auth 引数にタプルまたは認証クラスのインスタンスを渡します。

import httpx

# Basic 認証
basic_auth = ('your_username', 'your_password')
url_basic = 'https://httpbin.org/basic-auth/your_username/your_password'

try:
    response_basic = httpx.get(url_basic, auth=basic_auth)
    response_basic.raise_for_status() # エラーがあれば例外を発生させる
    print("Basic認証 成功")
    # print(response_basic.json())
except httpx.HTTPStatusError as e:
    print(f"Basic認証 失敗: ステータス {e.response.status_code}")
except httpx.RequestError as e:
    print(f"Basic認証 リクエストエラー: {e}")


# 独自認証クラスの例 (例: トークン認証)
class TokenAuth(httpx.Auth):
    def __init__(self, token):
        self._token = token

    def auth_flow(self, request):
        request.headers['Authorization'] = f'Bearer {self._token}'
        yield request

# custom_auth = TokenAuth('my-secret-token')
# response_custom = httpx.get('https://api.example.com/protected', auth=custom_auth)
# print(f"カスタム認証 ステータス: {response_custom.status_code}")

クライアントのカスタマイズ

httpx.Clienthttpx.AsyncClient をインスタンス化する際に、様々なオプションを指定して動作をカスタマイズできます。

  • base_url: ベースURL。相対パスでリクエストを送る際に便利。
  • headers: すべてのリクエストに適用されるデフォルトヘッダー。
  • cookies: すべてのリクエストに適用されるデフォルトクッキー。
  • params: すべてのリクエストに適用されるデフォルトクエリパラメータ。
  • timeout: デフォルトのタイムアウト設定 (httpx.Timeout オブジェクト)。
  • limits: 接続数の制限 (httpx.Limits オブジェクト)。同時接続数やキープアライブ接続数を制御。
  • proxies: プロキシ設定。
  • verify: SSL/TLS 証明書の検証設定 (True, False, または証明書ファイルのパス)。
  • cert: クライアント証明書の設定。
  • http1, http2: 使用するHTTPバージョンを制御。
  • transport: カスタムトランスポートクラスのインスタンス。
  • event_hooks: イベントフック関数。
  • follow_redirects: デフォルトのリダイレクト追従設定。
import httpx

# カスタマイズ例
limits = httpx.Limits(max_connections=100, max_keepalive_connections=20)
timeout = httpx.Timeout(10.0, connect=5.0)
headers = {"X-Client-Name": "MyCustomClient"}
# proxy_url = "http://user:password@myproxy.com:8080" # プロキシの例

client = httpx.Client(
    base_url="https://httpbin.org/",
    headers=headers,
    timeout=timeout,
    limits=limits,
    # proxies=proxy_url, # プロキシを使う場合
    follow_redirects=True,
    http2=False # HTTP/1.1 を強制する場合
)

try:
    with client: # with 文で使う
        response = client.get("/headers") # base_url からの相対パス
        response.raise_for_status()
        print("カスタムクライアントでのリクエスト成功:")
        # import json
        # print(json.dumps(response.json(), indent=2, ensure_ascii=False))
        # ヘッダーに 'X-Client-Name': 'MyCustomClient' が含まれているか確認
        received_headers = response.json().get('headers', {})
        print(f"X-Client-Name ヘッダー: {received_headers.get('X-Client-Name')}")

except httpx.RequestError as e:
    print(f"リクエストエラー: {e}")

イベントフック (Event Hooks)

リクエストの送信前やレスポンスの受信後などに特定の関数(フック)を呼び出すことができます。ロギング、モニタリング、リクエスト/レスポンスの改変などに利用できます。

event_hooks 引数に、イベント名 ('request' または 'response') と呼び出す関数のリストをマッピングした辞書を渡します。

import httpx
import time

def log_request(request):
    print(f"[Request Hook] > {request.method} {request.url}")
    # リクエストヘッダーにタイムスタンプを追加する例
    request.headers['X-Request-Timestamp'] = str(time.time())

def log_response(response):
    request = response.request # 対応するリクエストオブジェクトを取得
    elapsed = response.elapsed.total_seconds()
    print(f"[Response Hook] < {request.method} {request.url} - Status {response.status_code} ({elapsed:.2f}s)")
    # レスポンスヘッダーを確認する例
    # print(f"    Server Header: {response.headers.get('Server')}")

# イベントフックを設定してクライアントを作成
event_hooks = {'request': [log_request], 'response': [log_response]}

with httpx.Client(event_hooks=event_hooks) as client:
    print("イベントフック付きでリクエストを送信します...")
    client.get('https://httpbin.org/get')
    client.post('https://httpbin.org/post', data={'key': 'value'})
    print("リクエスト完了。")

Transport API と Mounting

httpx は低レベルの Transport API を提供しており、リクエストの送信方法を完全にカスタマイズできます。例えば、特定のドメインに対して異なる設定(プロキシ、SSL検証など)を持つトランスポートを「マウント」することが可能です。

これは非常に高度な機能であり、特定のユースケース(例: Microservices 環境で特定の内部サービスへの通信方法を変える)で役立ちます。

import httpx

# 例: example.com へのリクエストのみ特定のプロキシを使う Transport
custom_transport = httpx.HTTPTransport(proxy="http://specific-proxy.com:8080")
# 例: ローカルの WSGI/ASGI アプリケーション用 Transport
# app = MyWSGIApp()
# wsgi_transport = httpx.WSGITransport(app=app)

# Client に Transport をマウントする
# デフォルトの Transport は通常のインターネットアクセス用
# 特定のプレフィックスにマッチする場合、マウントした Transport が使われる
mounts = {
    "all://example.com": custom_transport,
    # "http://internal-service": wsgi_transport # WSGIアプリをマウントする例
}

with httpx.Client(mounts=mounts) as client:
    # このリクエストは custom_transport (プロキシ経由) で送信される
    response_example = client.get("https://example.com")
    print(f"example.com status: {response_example.status_code}")

    # このリクエストはデフォルトの Transport で送信される
    response_httpbin = client.get("https://httpbin.org/get")
    print(f"httpbin.org status: {response_httpbin.status_code}")

    # このリクエストは wsgi_transport で処理される (もしマウントされていれば)
    # response_internal = client.get("http://internal-service/status")
    # print(f"internal-service status: {response_internal.status_code}")

🎉 まとめ

httpx は、Python における HTTP 通信のための強力でモダンなライブラリです。requests の使いやすさを引き継ぎつつ、非同期処理HTTP/2・HTTP/3 という現代的な Web 技術に対応している点が大きな魅力です。

特に、非同期 I/O が求められるアプリケーションや、最新のプロトコルを活用してパフォーマンスを向上させたい場合に、httpx は非常に有効な選択肢となります。タイムアウト、認証、プロキシ、イベントフック、トランスポート API など、豊富なカスタマイズオプションも提供されており、複雑な要件にも対応可能です。

もしあなたが Python で Web API を利用したり、Web スクレイピングを行ったり、あるいは非同期処理に関心があるなら、ぜひ httpx を試してみてください。きっとそのパワフルさと柔軟性に満足するはずです!💪

公式ドキュメント (https://www.python-httpx.org/) には、さらに詳細な情報や使用例が豊富に掲載されていますので、ぜひ参照してください。

コメント

タイトルとURLをコピーしました