本文 AI 產出,尚未審核

FastAPI – 例外與錯誤處理:自訂例外處理器(exception_handler

簡介

在 Web API 開發中,例外(Exception)錯誤回應是不可避免的。若不妥善處理,使用者將看到不友善的 500 錯誤頁面,開發者也難以追蹤錯誤根源。FastAPI 內建了完整的例外處理機制,讓我們可以以統一、可讀的方式回傳錯誤訊息、記錄日誌,甚至根據不同例外類型回傳不同的 HTTP 狀態碼。

本單元聚焦於 自訂例外處理器exception_handler),說明如何在 FastAPI 中註冊、實作與應用自訂的錯誤回應。文章從概念說明、實作範例,到常見陷阱與最佳實踐,提供從入門到中階開發者都能直接上手的完整指南。


核心概念

1. 為什麼需要自訂例外處理器?

  • 統一錯誤格式:前端或第三方系統只要依照固定的 JSON 結構解析,即可快速取得錯誤代碼與訊息。
  • 隱藏內部實作:避免將堆疊追蹤或資料庫錯誤直接暴露給使用者,提升系統安全性。
  • 自動記錄:在例外發生時即寫入日誌,方便日後除錯與監控。

2. FastAPI 的例外處理流程

  1. 路由函式執行時拋出例外(raise)。
  2. FastAPI 先檢查 內建例外處理器(如 HTTPException)。
  3. 若沒有對應的處理器,會搜尋 使用者自訂的例外處理器(透過 app.exception_handler 註冊)。
  4. 找到後呼叫該處理器,回傳 ResponseJSONResponse 給客戶端。

重點:自訂例外處理器必須接受兩個參數 (request: Request, exc: Exception),並回傳 Response 物件。

3. 註冊自訂例外處理器的語法

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

app = FastAPI()

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    # 這裡的程式碼會在所有 HTTPException 被拋出時執行
    return JSONResponse(
        status_code=exc.status_code,
        content={"detail": exc.detail, "path": request.url.path},
    )

技巧:若想一次註冊多個例外,只要多寫幾個 @app.exception_handler 裝飾器即可,FastAPI 會依照例外類型自動匹配。


程式碼範例

以下示範 5 個常見且實用的自訂例外處理器,涵蓋從簡單的 HTTPException 到自訂業務例外、驗證錯誤與全域捕獲。

範例 1️⃣ 統一處理 HTTPException

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

app = FastAPI()

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    """
    把所有 HTTPException 包裝成統一的 JSON 格式。
    前端只需檢查 `code` 與 `message` 兩個欄位。
    """
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "code": exc.status_code,
            "message": exc.detail,
            "path": str(request.url),
        },
    )

範例 2️⃣ 自訂業務例外(ItemNotFoundError

class ItemNotFoundError(Exception):
    """當要查詢的商品不存在時拋出此例外"""

    def __init__(self, item_id: int):
        self.item_id = item_id
        self.message = f"Item with id {item_id} not found"
        super().__init__(self.message)

@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
    return JSONResponse(
        status_code=404,
        content={
            "code": 1001,
            "error": "ItemNotFound",
            "detail": exc.message,
            "item_id": exc.item_id,
        },
    )

範例 3️⃣ 捕捉 Pydantic 驗證錯誤(RequestValidationError

from fastapi.exceptions import RequestValidationError
from fastapi.encoders import jsonable_encoder

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    """
    把 Pydantic 的驗證錯誤轉成更易讀的結構。
    """
    errors = [{"loc": err["loc"], "msg": err["msg"], "type": err["type"]} for err in exc.errors()]
    return JSONResponse(
        status_code=422,
        content=jsonable_encoder({
            "code": 1002,
            "error": "ValidationError",
            "detail": errors,
            "path": str(request.url),
        })
    )

範例 4️⃣ 全域捕獲未處理的例外(Exception

import traceback
import logging

logger = logging.getLogger("uvicorn.error")

@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    """
    捕獲所有未被其他 handler 處理的例外,寫入日誌並回傳 500。
    """
    tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
    logger.error(f"Unhandled exception: {tb_str}")

    return JSONResponse(
        status_code=500,
        content={
            "code": 1000,
            "error": "InternalServerError",
            "detail": "系統發生未預期的錯誤,請稍後再試。",
        },
    )

範例 5️⃣ 依需求動態註冊例外處理器(插件化)

def register_custom_handler(app: FastAPI, exc_class: type[Exception], status_code: int, code: int):
    @app.exception_handler(exc_class)
    async def custom_handler(request: Request, exc: Exception):
        return JSONResponse(
            status_code=status_code,
            content={"code": code, "error": exc_class.__name__, "detail": str(exc)},
        )

# 使用方式
class PermissionDeniedError(Exception):
    pass

register_custom_handler(app, PermissionDeniedError, 403, 2001)

以上範例示範了 從單一例外到全域捕獲,以及 動態註冊 的技巧,幾乎涵蓋了實務開發中會遇到的所有需求。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記 await 自訂處理器若內含非同步 I/O(如寫入資料庫、發送 Slack 訊息)卻未使用 await,會產生未預期的執行順序或警告。 確保處理器是 async def,並在需要的地方使用 await
返回非 Response 物件 有時直接回傳字典或 None 會導致 FastAPI 自動轉換為 JSONResponse,但缺少自訂的 HTTP 狀態碼。 明確回傳 JSONResponsePlainTextResponse 或自訂 Response
例外類別層級不明確 若同時註冊了父類別與子類別的處理器,FastAPI 會優先匹配最具體的子類別。 利用繼承結構設計例外,避免同一層級重複註冊。
過度捕獲 Exception 全域捕獲會把所有錯誤都變成 500,開發者可能失去定位具體錯誤的機會。 只在最後一步使用 Exception 捕獲,並在日誌中完整記錄堆疊資訊。
錯誤訊息洩漏 把原始例外文字直接回傳給使用者,可能會暴露資料庫結構或內部實作。 在回傳給前端前,過濾或重新組合錯誤訊息,只保留必要資訊。

最佳實踐

  1. 統一錯誤格式:設計一個全域的錯誤 schema(例如 code, error, detail, path),所有自訂處理器都遵守此格式。
  2. 分層處理:先針對業務例外(Domain Error)寫專屬處理器,再用 HTTPException 處理常見的 4xx/5xx,最後使用 Exception 捕獲未知錯誤。
  3. 日誌與監控:在每個處理器內部寫入結構化日誌(如 JSON),配合 ELK、Prometheus 等監控系統,才能快速定位問題。
  4. 測試例外路徑:使用 TestClient 撰寫單元測試,確保每個例外都能正確回傳預期的 HTTP 狀態碼與 JSON 結構。
from fastapi.testclient import TestClient

client = TestClient(app)

def test_item_not_found():
    response = client.get("/items/999")
    assert response.status_code == 404
    assert response.json()["code"] == 1001

實際應用場景

1. 電子商務平台:商品查詢失敗

當使用者查詢不存在的商品時,拋出 ItemNotFoundError,自訂處理器回傳 code: 1001,前端即可直接顯示「商品不存在」的提示,而不必自行判斷 404。

2. 金融 API:權限驗證

在每個受保護的路由中,若使用者的 JWT 權限不足,拋出 PermissionDeniedError。統一的處理器回傳 403 並附上錯誤代碼,前端可以根據代碼顯示「您沒有操作此資源的權限」。

3. 大數據批次服務:驗證錯誤

大量資料上傳時,若欄位不符合 Pydantic 模型,RequestValidationError 會被觸發。自訂處理器把錯誤轉成 errors 陣列,前端只需要迭代顯示每個欄位的錯誤訊息,提升使用者體驗。

4. 微服務架構:跨服務錯誤傳遞

在微服務間呼叫時,若下游服務回傳錯誤,將其封裝成 DownstreamServiceError,自訂處理器將錯誤碼映射為統一的 code: 3001,讓呼叫方可以根據代碼決定是否重試或回退。

5. 監控與告警系統

全域 Exception 捕獲器寫入 SentryDatadog,同時回傳給前端通用的 500 錯誤訊息。這樣既不洩漏內部細節,又能在後端即時收到告警。


總結

  • 自訂例外處理器 是 FastAPI 提供的強大功能,讓我們能在拋出例外的同時,回傳一致且安全的錯誤訊息。
  • 核心概念包括:註冊方式 (@app.exception_handler)、處理器簽名 (request, exc)、以及回傳 Response
  • 透過 5 個實務範例(統一 HTTPException、業務例外、驗證錯誤、全域捕獲、動態註冊),讀者可以快速在自己的專案中套用。
  • 常見陷阱如忘記 await、過度捕獲、錯誤訊息洩漏等,只要遵守 最佳實踐(統一錯誤格式、分層處理、日誌監控、測試),即可建立健全的錯誤處理機制。
  • 電子商務、金融、微服務 等多種實際場景下,自訂例外處理器不僅提升 API 的可用性,也減少前端的錯誤處理負擔,讓整體系統更具彈性與可維護性。

從今天開始,把例外處理視為 API 設計的一部份,使用 FastAPI 的 exception_handler 為你的服務打造 安全、友好且易於除錯 的錯誤回應機制吧!