FastAPI – 例外與錯誤處理:自訂例外處理器(exception_handler)
簡介
在 Web API 開發中,例外(Exception)與錯誤回應是不可避免的。若不妥善處理,使用者將看到不友善的 500 錯誤頁面,開發者也難以追蹤錯誤根源。FastAPI 內建了完整的例外處理機制,讓我們可以以統一、可讀的方式回傳錯誤訊息、記錄日誌,甚至根據不同例外類型回傳不同的 HTTP 狀態碼。
本單元聚焦於 自訂例外處理器(exception_handler),說明如何在 FastAPI 中註冊、實作與應用自訂的錯誤回應。文章從概念說明、實作範例,到常見陷阱與最佳實踐,提供從入門到中階開發者都能直接上手的完整指南。
核心概念
1. 為什麼需要自訂例外處理器?
- 統一錯誤格式:前端或第三方系統只要依照固定的 JSON 結構解析,即可快速取得錯誤代碼與訊息。
- 隱藏內部實作:避免將堆疊追蹤或資料庫錯誤直接暴露給使用者,提升系統安全性。
- 自動記錄:在例外發生時即寫入日誌,方便日後除錯與監控。
2. FastAPI 的例外處理流程
- 路由函式執行時拋出例外(
raise)。 - FastAPI 先檢查 內建例外處理器(如
HTTPException)。 - 若沒有對應的處理器,會搜尋 使用者自訂的例外處理器(透過
app.exception_handler註冊)。 - 找到後呼叫該處理器,回傳
Response或JSONResponse給客戶端。
重點:自訂例外處理器必須接受兩個參數
(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 狀態碼。 |
明確回傳 JSONResponse、PlainTextResponse 或自訂 Response。 |
| 例外類別層級不明確 | 若同時註冊了父類別與子類別的處理器,FastAPI 會優先匹配最具體的子類別。 | 利用繼承結構設計例外,避免同一層級重複註冊。 |
過度捕獲 Exception |
全域捕獲會把所有錯誤都變成 500,開發者可能失去定位具體錯誤的機會。 | 只在最後一步使用 Exception 捕獲,並在日誌中完整記錄堆疊資訊。 |
| 錯誤訊息洩漏 | 把原始例外文字直接回傳給使用者,可能會暴露資料庫結構或內部實作。 | 在回傳給前端前,過濾或重新組合錯誤訊息,只保留必要資訊。 |
最佳實踐:
- 統一錯誤格式:設計一個全域的錯誤 schema(例如
code,error,detail,path),所有自訂處理器都遵守此格式。 - 分層處理:先針對業務例外(Domain Error)寫專屬處理器,再用
HTTPException處理常見的 4xx/5xx,最後使用Exception捕獲未知錯誤。 - 日誌與監控:在每個處理器內部寫入結構化日誌(如 JSON),配合 ELK、Prometheus 等監控系統,才能快速定位問題。
- 測試例外路徑:使用
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 捕獲器寫入 Sentry 或 Datadog,同時回傳給前端通用的 500 錯誤訊息。這樣既不洩漏內部細節,又能在後端即時收到告警。
總結
- 自訂例外處理器 是 FastAPI 提供的強大功能,讓我們能在拋出例外的同時,回傳一致且安全的錯誤訊息。
- 核心概念包括:註冊方式 (
@app.exception_handler)、處理器簽名(request, exc)、以及回傳Response。 - 透過 5 個實務範例(統一 HTTPException、業務例外、驗證錯誤、全域捕獲、動態註冊),讀者可以快速在自己的專案中套用。
- 常見陷阱如忘記
await、過度捕獲、錯誤訊息洩漏等,只要遵守 最佳實踐(統一錯誤格式、分層處理、日誌監控、測試),即可建立健全的錯誤處理機制。 - 在 電子商務、金融、微服務 等多種實際場景下,自訂例外處理器不僅提升 API 的可用性,也減少前端的錯誤處理負擔,讓整體系統更具彈性與可維護性。
從今天開始,把例外處理視為 API 設計的一部份,使用 FastAPI 的
exception_handler為你的服務打造 安全、友好且易於除錯 的錯誤回應機制吧!