Python | Web / API:API エラー処理

Python Python
スポンサーリンク

概要(APIエラー処理は「失敗を見落とさない・止めない・壊さない」ための型づくり)

APIは必ず失敗します。通信が詰まる、サーバが落ちる、仕様が変わる、レート制限に当たる。だから「すべて成功前提」で書くのではなく、「失敗を検知して止めずに安全側へ倒す」型を最初から組み込みます。核心は、ステータスコードの検査と例外化、タイムアウト、応答形式の確認、メッセージのログ化、必要に応じた再試行(慎重に)、そして「非冪等な操作は二重実行させない」ことです。


ここが重要(最小セットの安全対策)

成功・失敗を例外で判定する

import requests

resp = requests.get("https://httpbin.org/status/500", timeout=5)
try:
    resp.raise_for_status()  # 4xx/5xx を HTTPError にする
except requests.HTTPError as e:
    print("HTTPエラー:", e)
Python

raise_for_status を必ず呼ぶと、失敗を見落とさずに「例外として扱う」ことができます。以降の処理が壊れたデータで続行される事故を防げます。

タイムアウトで「待ち続け」を止める

import requests

try:
    r = requests.get("https://httpbin.org/delay/3", timeout=1.5)
except requests.Timeout:
    print("タイムアウトにより中断")
Python

timeout は毎回付けます。ネットワーク不調で処理が固まる問題を根本から避けられます。

応答形式を確認してから JSON を読む

import requests

r = requests.get("https://httpbin.org/anything", timeout=5)
r.raise_for_status()
ctype = r.headers.get("Content-Type", "")
if "application/json" in ctype:
    data = r.json()
else:
    print("JSON以外の応答:", ctype, r.text[:120])
Python

APIがエラー時にHTMLやテキストを返すことは珍しくありません。Content-Type を見てから json() を呼ぶと安全です。


代表的な失敗と対策(実務でよく遭遇するもの)

クライアント側の例外を押さえる

import requests

url = "https://example.invalid"
try:
    r = requests.get(url, timeout=5)
    r.raise_for_status()
except requests.Timeout:
    print("タイムアウト")
except requests.ConnectionError:
    print("接続エラー")
except requests.HTTPError as e:
    print("HTTPエラー:", e)
except requests.RequestException as e:
    print("その他のリクエスト例外:", e)
Python

Timeout、ConnectionError、HTTPError を個別に扱い、最後は親クラス RequestException で網羅します。

サーバ側のステータスに応じた分岐

import requests

r = requests.get("https://httpbin.org/status/429", timeout=5)
if r.status_code == 429:
    retry_after = r.headers.get("Retry-After")
    print("レート制限。待機指示:", retry_after)
elif 500 <= r.status_code < 600:
    print("サーバ一時不調。後で再試行を検討")
else:
    r.raise_for_status()
Python

429 はレート制限、5xx はサーバの一時不調の可能性が高いので、再試行の対象になります。一方で 4xx はクライアントの指定が間違っていることが多く、再試行より原因修正が先です。

エラー応答のJSONから意味を取り出す

import requests

r = requests.get("https://httpbin.org/status/400", timeout=5)
if r.status_code >= 400:
    try:
        err = r.json()
        print("エラー詳細:", err)
    except ValueError:
        print("テキストエラー:", r.text[:120])
Python

APIはエラー時に JSON で code・message を返すことが多いので、そこから「ユーザへ見せる文言」「ログへ出す詳細」を分けて使います。


再試行(リトライ)の設計(副作用に配慮したやり方)

安全な自動リトライ(GETなど冪等な処理)

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(
    total=3,
    backoff_factor=0.5,               # 0.5, 1.0, 2.0... と間隔を伸ばす
    status_forcelist=[500, 502, 503, 504]  # 一時不調のみ対象
)
session.mount("https://", HTTPAdapter(max_retries=retry))

resp = session.get("https://httpbin.org/status/503", timeout=5)
print("最終ステータス:", resp.status_code)
Python

冪等(同じ要求を繰り返しても状態が変わらない)な GET は、5xx に限って指数バックオフでリトライすると安定します。

POST のリトライは慎重に(重複実行の回避)

課金や登録などの POST は非冪等です。再送すると二重実行になり得ます。サーバが idempotency-key(同一キーなら一度だけ処理)に対応している場合のみ、キーを付けて安全に再試行します。


予防と検証(壊れないための前工程)

入力の型と必須キーを軽くバリデート

def validate_payload(p):
    assert isinstance(p, dict), "辞書が必要"
    assert "user" in p and "score" in p, "必須キーが不足"

payload = {"user": "taro", "score": 88}
validate_payload(payload)
Python

送る前に最小限の検証を入れると、400系のエラーを減らせます。重いスキーマ検証は後からでも良いですが、必須キーの確認だけは即効性があります。

クエリ・ヘッダー・Content-Type の整合を取る

import requests

params = {"q": "python", "page": 1}
headers = {"Accept": "application/json"}
r = requests.get("https://httpbin.org/get", params=params, headers=headers, timeout=5)
r.raise_for_status()
Python

JSON を期待するなら Accept を合わせ、送るときは json= を使い Content-Type を自動で正しく付与します。


ログと通知(原因追跡できる形を残す)

最低限のログ項目

URL、メソッド、ステータスコード、主要ヘッダー(Retry-After など)、短縮した応答本文、例外メッセージ。個人情報・秘密鍵はログに残さないことを徹底します。

失敗の見せ方

ユーザ向けには短く状況を伝えるメッセージ(「混雑しています。少し待って再度お試しください」)。ログには技術詳細(「HTTP 503、再試行3回後に失敗」)。同じメッセージを使い回すのではなく、状況に応じて切り替えます。


例題で身につける(定番から実務まで)

例題1 成功可否とJSON判定の基本線

import requests

r = requests.get("https://httpbin.org/json", timeout=5)
r.raise_for_status()
ctype = r.headers.get("Content-Type", "")
if "application/json" in ctype:
    print("OK:", list(r.json().keys()))
else:
    print("想定外の応答形式:", ctype)
Python

例題2 例外を網羅して安全側へ倒す

import requests

url = "https://httpbin.org/status/500"
try:
    resp = requests.get(url, timeout=3)
    resp.raise_for_status()
except requests.Timeout:
    print("タイムアウト。後で再試行")
except requests.ConnectionError:
    print("接続に失敗。ネットワーク確認")
except requests.HTTPError as e:
    print("HTTPエラー:", e)
except requests.RequestException as e:
    print("予期せぬ例外:", e)
Python

例題3 レート制限を尊重して待機

import time, requests

r = requests.get("https://httpbin.org/status/429", timeout=5)
if r.status_code == 429:
    ra = r.headers.get("Retry-After")
    wait = int(ra) if ra and ra.isdigit() else 2
    print(f"{wait}秒待機後に再試行")
    time.sleep(wait)
    # 実際はここで安全に再試行
Python

例題4 セッション+指数バックオフで安定化(GET)

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

sess = requests.Session()
sess.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])))

resp = sess.get("https://httpbin.org/status/503", timeout=5)
print("ステータス:", resp.status_code)
Python

まとめ

APIエラー処理の土台は、raise_for_statusで失敗を例外化し、timeoutで固まりを防ぎ、Content-Typeで応答形式を確認すること。429や5xxには慎重な再試行を、POSTなど非冪等操作には重複実行の回避策を。入力バリデーションで自分由来の 4xx を減らし、ログは原因追跡に十分でありつつ機密を残さない。これらを「型」として最初から組み込めば、初心者でも短く、壊れないAPI連携が自然に書けるようになります。

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