Python | Web フレームワーク:例外ハンドラ

Python
スポンサーリンク

概要(例外ハンドラは「エラーを受け止めて整える役」)

例外ハンドラは、

「アプリの中で投げられた例外をキャッチして、
クライアントに返す“きれいなエラーレスポンス”に変換する仕組み」

です。

Python で普通にコードを書いていると、エラーが起きたら raise で例外が飛びます。
Web フレームワークでは、その例外をそのままブラウザに出すのではなく、

どんな HTTP ステータスコードにするか
どんな JSON 形式で返すか
ログをどう残すか

を「例外ハンドラ」で決めていきます。

FastAPI を例に、初心者向けにじっくりかみ砕いて説明します。


例外ハンドラがないときの世界(素の例外の流れ)

普通に例外を投げたらどうなるか

FastAPI で何も特別な設定をせず、普通に例外が起きるコードを書いてみます。

from fastapi import FastAPI

app = FastAPI()

@app.get("/zero")
def divide_by_zero():
    return 1 / 0
Python

/zero にアクセスすると、Python の ZeroDivisionError が発生します。

FastAPI は内部でこの例外を捕まえ、
HTTP 500(Internal Server Error)として、
簡単なエラー情報を JSON で返します。

クライアント側からは、

「なんかサーバーがおかしい」
ということしか分かりません。

開発中ならこれで十分なこともありますが、本番環境では不十分です。

ユーザーに意味の分かるエラーを返したい
ログに詳細な情報を残したい
同じ種類のエラーは同じ形で返したい

こういった要件が出てきたときに使うのが「例外ハンドラ」です。


FastAPI の例外ハンドラの基本構造

@app.exception_handler で「この例外が来たらこう返す」を登録する

FastAPI では、特定の例外クラスに対して
「この例外をどう HTTP レスポンスに変えるか」を登録できます。

最低限の形はこうです。

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class MyError(Exception):
    pass

@app.exception_handler(MyError)
async def my_error_handler(request: Request, exc: MyError):
    return JSONResponse(
        status_code=400,
        content={"detail": "MyError が発生しました"},
    )
Python

大事なポイントを分解します。

ひとつ目は、@app.exception_handler(MyError) の部分です。
「MyError という例外がどこかで投げられたら、この関数で処理します」という登録です。

ふたつ目は、ハンドラ関数のシグネチャです。

第一引数: request: Request(どのリクエストで起きたか)
第二引数: exc: MyError(実際に投げられた例外オブジェクト)

この 2 つを受け取って、Response(JSONResponse など)を返します。

みっつ目は、ハンドラ内で「HTTPステータスと JSON を自由に決められる」ことです。
ここでは仮に 400 を返していますが、状況に応じて 403, 404, 500 などを選べます。

この登録をしておくと、アプリ内のどこかで

raise MyError("なんかおかしい")
Python

とした瞬間、
my_error_handler が呼び出され、統一されたエラー JSON を返します。


HTTPException 専用の例外ハンドラと標準の動き

HTTPException は FastAPI 標準でハンドリングされる

FastAPI では、よく使う HTTPException については、
すでに「内蔵の例外ハンドラ」が登録されています。

例えば、

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id != 1:
        raise HTTPException(status_code=404, detail="User not found")
    return {"id": 1, "name": "Taro"}
Python

/users/2 にアクセスすると、
ステータス 404 と

{"detail": "User not found"}

が自動で返ります。

これは FastAPI の内部で、

@app.exception_handler(HTTPException)
async def http_exception_handler(...):
    ...
Python

のようなハンドラが定義されているイメージです(実際の実装はもっと複雑ですが)。

つまり、

HTTPException を自分で raise した場合は、
特にハンドラを書かなくても、それなりにきれいなエラーレスポンスが返る

ということです。

HTTPException のハンドラを自分で上書きすることもできる

もし標準の挙動をカスタマイズしたければ、
自分で HTTPException 用のハンドラを書いて上書きできます。

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error_code": f"HTTP_{exc.status_code}",
            "message": exc.detail,
        },
    )
Python

こうすると、全ての HTTPException が

{
  "error_code": "HTTP_404",
  "message": "User not found"
}

のような統一フォーマットで返るようになります。

標準だと {"detail": ...} ですが、
自分のエラーフォーマットに揃えたいときに有効です。


独自の例外クラス+例外ハンドラで「業務エラー」をきれいに扱う

ビジネスロジック専用の例外を作る

実際の業務アプリでは、

ユーザーが見つからない
在庫不足で注文できない
日付の範囲指定がおかしい

といった「業務的なエラー」がたくさん出てきます。

それを全部 HTTPException で書いてしまうと、
「どこで何が起きているのか」が分かりづらくなります。

そこで、独自の例外クラスを作ります。

class UserNotFoundError(Exception):
    def __init__(self, user_id: int):
        self.user_id = user_id
Python

そして、例外ハンドラを登録します。

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(UserNotFoundError)
async def user_not_found_handler(request: Request, exc: UserNotFoundError):
    return JSONResponse(
        status_code=404,
        content={
            "error_code": "USER_NOT_FOUND",
            "message": f"User {exc.user_id} not found",
        },
    )
Python

エンドポイント側は、業務ロジックに集中して書けます。

fake_users = {1: {"id": 1, "name": "Taro"}}

@app.get("/users/{user_id}")
def get_user(user_id: int):
    user = fake_users.get(user_id)
    if user is None:
        raise UserNotFoundError(user_id)
    return user
Python

ここでの重要ポイントは二つです。

一つ目は、「HTTP のことを考えずに業務用の例外を投げている」こと。
エンドポイント内では、UserNotFoundError が HTTP 404 になるかどうかを意識していません。

二つ目は、「エラーレスポンスの形が一箇所で決まっている」こと。
エンドポイントが増えても、エラーフォーマットやステータスコードを変えたいときは
ハンドラだけ触れば済みます。

これが「例外ハンドラで整える」という設計の強さです。


予期しない例外をまとめて受ける「最後の砦」ハンドラ

Exception 全体のハンドラでログ+汎用 500 を返す

どれだけ頑張っても、
コードのバグや外部サービス障害などの「予期しない例外」は起きます。

それを全部 500 で返すだけでもいいのですが、
どうせなら

ログにスタックトレースを残しておきたい
クライアントには「内部エラー」とだけ伝えたい(内部情報は隠したい)

ということが多いはずです。

そこで、Exception 用のハンドラを用意します。

import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()
logger = logging.getLogger(__name__)

@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    logger.exception(f"Unhandled error: {exc}")
    return JSONResponse(
        status_code=500,
        content={
            "error_code": "INTERNAL_SERVER_ERROR",
            "message": "予期しないエラーが発生しました。",
        },
    )
Python

このハンドラは、

HTTPException や独自例外など、
すでに個別ハンドラがある例外を除く「その他全部」

を受け止めます。

やっていることはとてもシンプルです。

ログに logger.exception で詳細(スタックトレース)を残す
クライアントには 500 と汎用メッセージだけ返す

これを入れておくだけで、

ユーザーには内部情報を漏らさず
開発者はログから原因を追える

という「最後の防衛ライン」ができます。


例外ハンドラ設計で大事な考え方

「どの例外が、どのハンドラで処理されるか」をはっきりさせる

例外ハンドラは「どの例外クラスに対して定義したか」で選ばれます。

例えば、

UserNotFoundError → user_not_found_handler
HTTPException → custom_http_exception_handler
Exception → generic_exception_handler

のようにしておくと、
優先度としては「より具体的な例外クラス」が優先されます。

UserNotFoundError が投げられたとき
→ UserNotFoundError のハンドラが呼ばれる(Exception のハンドラより優先)

ValueError など、何もハンドラを定義していない例外
→ Exception のハンドラが受け止める

こういった流れを頭の中で整理しておくと、
「この例外はどのハンドラでどう処理されるべきか」を設計しやすくなります。

エラーレスポンスの「形」を統一する

例外ハンドラを使うときの重要な視点は、

「どんな JSON でエラーを返すか」を決めてしまうこと

です。

例えば、

成功時は普通にデータを返す
エラー時は常に

{
  "error_code": "SOME_CODE",
  "message": "人間向けの説明"
}

の形に揃える

という方針を決めたら、その形を返すハンドラを揃えていきます。

HTTPException 用ハンドラ
独自例外用ハンドラ
Exception 用「最後の砦」ハンドラ

全部でこの形にしておけば、
フロントエンドや他サービスから見たときに、
「エラーのときは error_code を見ればいい」と分かりやすくなります。


まとめ(例外ハンドラは「エラーの出口」を統一する仕組み)

Python の Web フレームワーク(FastAPI)における例外ハンドラを整理すると、こうなります。

  • 例外ハンドラは、特定の例外クラスが投げられたときに「どんな HTTP レスポンスに変換するか」を決める仕組みで、@app.exception_handler(SomeError) で登録する。
  • HTTPException など、FastAPI が標準でハンドリングしてくれる例外もあるが、自分でハンドラを定義すればレスポンス形式やログの残し方を自由にカスタマイズできる。
  • 独自の業務用例外(UserNotFoundError など)と、その例外専用のハンドラを組み合わせることで、「ビジネスロジック側は例外を投げるだけ」「HTTP のことはハンドラに任せる」というきれいな分離ができる。
  • Exception 用の汎用ハンドラを「最後の砦」として用意しておくと、予期しないエラーも一括でログ+500エラーとして扱え、ユーザーには内部情報を隠しつつ原因調査がしやすくなる。
  • エラーレスポンスの形(error_code, message など)をレスポンスモデルとして決め、それに従うハンドラを揃えていくことが、「壊れたときに強い API」を作る鍵になる。

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