FastAPI – 例外與錯誤處理:例外與中介層交互
簡介
在 Web API 開發中,例外(Exception)不只是程式碼錯誤的訊號,更是 向呼叫端傳遞錯誤資訊、維持服務品質的關鍵。
FastAPI 內建了強大的例外處理機制,配合 中介層(Middleware),可以在請求的最前端或最末端統一捕捉、記錄、轉換錯誤,讓 API 的行為更可預測、維護成本更低。
本篇文章將說明 例外與中介層如何互動,從最基礎的 HTTPException 到自訂例外與全域處理器,再談中介層在例外鏈結中的角色。內容以 繁體中文(台灣) 撰寫,適合剛入門的開發者,也能為已有 FastAPI 經驗的同學提供實務參考。
核心概念
1. FastAPI 的例外模型
FastAPI 以 Starlette 為底層框架,例外的傳遞遵循 ASGI 標準。主要概念如下:
| 概念 | 說明 |
|---|---|
| HTTPException | FastAPI 提供的內建例外,用來回傳 HTTP 狀態碼與錯誤訊息。 |
| RequestValidationError | 請求參數驗證失敗時自動拋出的例外,常與 Pydantic 結合。 |
| 自訂例外 | 開發者自行定義的類別,可在全域或路由層級註冊處理器。 |
| Exception Handler | 透過 app.exception_handler() 註冊的函式,用來把例外轉成 HTTP 回應。 |
| Middleware | 在請求/回應流程的前後插入自訂邏輯,亦可捕捉例外、執行日誌、事務管理等。 |
重點:例外的處理順序是 Middleware → 路由 → Exception Handler。若 Middleware 已捕捉例外並自行回應,後續的 Exception Handler 就不會被觸發。
2. 基本的 HTTPException
最常見的情境是驗證失敗或資源不存在時,直接拋出 HTTPException。
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
# 假設資料庫裡只有 ID 為 1~5 的項目
if item_id > 5:
# 拋出 404 Not Found,FastAPI 會自動把它轉成 JSON 回應
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id, "name": f"Item {item_id}"}
註解:
status_code為 HTTP 狀態碼,必須是 4xx/5xx,否則會被視為成功回應。detail會被放入回應的detail欄位,方便前端直接顯示。
3. 自訂例外類別
在大型專案中,往往需要更細緻的錯誤分類,例如 資料庫錯誤、授權失敗、外部 API 超時 等。這時可以自行定義例外類別,並在全域註冊對應的處理器。
class DatabaseError(Exception):
"""自訂資料庫例外,攜帶錯誤代碼與訊息"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
@app.exception_handler(DatabaseError)
async def database_error_handler(request, exc: DatabaseError):
# 這裡可以寫入日誌、整合錯誤代碼
return JSONResponse(
status_code=500,
content={"error_code": exc.code, "detail": exc.message}
)
使用方式:
@app.get("/users/{user_id}")
async def get_user(user_id: int):
try:
user = await db.get_user(user_id) # 假設此函式會拋出 DatabaseError
except DatabaseError as e:
# 直接 re‑raise,讓全域處理器接手
raise e
return user
要點:
- 自訂例外不必繼承
HTTPException,只要在exception_handler中回傳JSONResponse或其他Response即可。 - 透過
request參數,你可以取得原始請求資訊(如 URL、Headers),方便記錄或做條件分支。
4. 全域的 RequestValidationError 處理
FastAPI 會自動檢查路由參數、Body、Query 等,失敗時會拋出 RequestValidationError。如果想要統一回傳錯誤格式,可以覆寫預設的處理器。
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi import status
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
errors = [{"loc": err["loc"], "msg": err["msg"], "type": err["type"]} for err in exc.errors()]
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"code": "VALIDATION_ERROR", "errors": errors}
)
此範例會把原本的 422 回應改寫為:
{
"code": "VALIDATION_ERROR",
"errors": [
{"loc": ["body", "name"], "msg": "field required", "type": "value_error.missing"},
...
]
}
5. 中介層(Middleware)與例外的交互
5.1 為什麼要在 Middleware 捕捉例外?
- 統一日誌:所有請求的錯誤都可以在同一個地方寫入檔案或外部監控系統(如 Sentry、Datadog)。
- 事務(Transaction)管理:在資料庫或訊息佇列操作時,若發生例外需要 Rollback,Middleware 能在請求結束前完成。
- 回應封裝:即使下層路由沒有設定 Exception Handler,仍能保證回傳一致的 JSON 結構。
5.2 基本的錯誤捕捉 Middleware
import time
import traceback
from fastapi import Request
from fastapi.responses import JSONResponse
@app.middleware("http")
async def error_logging_middleware(request: Request, call_next):
start_time = time.time()
try:
response = await call_next(request) # 呼叫下游路由或其他 middleware
except Exception as exc:
# 例外被捕捉,此時還未有回應物件
elapsed = (time.time() - start_time) * 1000
# 記錄錯誤細節(可改為 logger)
print(f"[ERROR] {request.method} {request.url} - {exc!r} ({elapsed:.2f}ms)")
# 回傳統一的錯誤格式
return JSONResponse(
status_code=500,
content={"code": "INTERNAL_SERVER_ERROR", "detail": str(exc)}
)
# 若無例外,直接回傳原始回應
elapsed = (time.time() - start_time) * 1000
response.headers["X-Process-Time-ms"] = str(elapsed)
return response
說明:
call_next代表 下一層(路由或其他 Middleware)的處理結果。- 若
call_next期間拋出例外,Middleware 會 攔截 並自行產生回應,此時 之後的 Exception Handler 不會再被觸發。 - 透過
request可以取得完整的請求資訊,幫助除錯與分析。
5.3 結合自訂例外與 Middleware
假設我們想在所有 DatabaseError 發生時,除了回傳 500,還要 自動回滾資料庫交易。可以在 Middleware 中檢測例外類型,再決定如何處理。
@app.middleware("http")
async def db_transaction_middleware(request: Request, call_next):
async with db.session() as session: # 假設使用 async context manager
try:
# 把 session 注入到 request.state,讓路由可以存取
request.state.db = session
response = await call_next(request)
except DatabaseError as db_exc:
await session.rollback() # 事務回滾
# 交給全域 handler 處理(或直接回傳)
return JSONResponse(
status_code=500,
content={"code": "DB_ERROR", "detail": db_exc.message}
)
except Exception as exc:
await session.rollback()
raise exc # 交給其他 middleware 或 global handler
else:
await session.commit() # 成功則提交
return response
在路由中只要使用 request.state.db 即可,例外的捕捉與事務處理已在 Middleware 完成,路由本身保持 乾淨。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
| 例外被多次捕捉 | 同一例外同時在 Middleware、Exception Handler、路由內捕捉,會導致回應不一致或重複日誌。 | 先決定捕捉層級:若要全域日誌,放在 Middleware;若要自訂回應格式,放在 Exception Handler;路由內只捕捉需要特別處理的例外。 |
| 忘記回傳 Response | Middleware 中捕捉例外卻沒有回傳 Response,導致 ASGI 無回應錯誤。 |
捕捉例外後 一定要回傳 JSONResponse、PlainTextResponse 或自訂 Response。 |
使用 raise HTTPException 於非 HTTP 層 |
在非請求上下文(如背景任務)拋出 HTTPException,FastAPI 無法自動轉換。 |
在背景任務中使用自訂例外,或自行呼叫 JSONResponse。 |
| 過度依賴全域 Exception Handler | 把所有錯誤都交給全域處理器,可能隱藏業務邏輯錯誤。 | 針對不同錯誤類型 建立 專屬的 handler(如驗證、授權、資料庫),保持錯誤資訊的語意。 |
未設定 response_model |
即使例外被捕捉,回傳的 JSON 結構仍可能不符合 API 文件。 | 統一錯誤模型:建立 ErrorResponse Pydantic 模型,讓所有例外回傳相同結構。 |
小技巧
- 使用
logger.exception():自動把 traceback 寫入日誌,適合在 Middleware 捕捉例外時使用。 - 在
request.state中傳遞上下文:如 DB session、用戶資訊,避免在每個路由都重複取得。 - 測試例外流程:使用
TestClient的raise_server_exceptions=False來驗證錯誤回應是否符合預期。
實際應用場景
| 場景 | 為何需要例外+Middleware 結合 | 典型實作 |
|---|---|---|
| 認證/授權失敗 | 需要在最前端檢查 token,若無效立即回應 401,並寫入安全日誌。 | 在 auth_middleware 中驗證 JWT,拋出自訂 AuthError,全域 handler 把它轉成 {"code":"UNAUTHORIZED","detail":...}。 |
| 外部 API 超時 | 呼叫第三方服務時可能卡住,若超時要返回 504 並記錄呼叫資訊。 | 在路由內使用 asyncio.wait_for(),捕捉 TimeoutError,拋出 ExternalServiceError;Middleware 捕捉此例外、寫入外部監控、回傳統一錯誤。 |
| 批次資料匯入 | 大批量寫入 DB,若其中一筆失敗需要回滾,並返回錯誤明細。 | 使用 transaction_middleware 包住整個請求,遇到 DatabaseError 時自動 rollback,並把失敗筆數回傳給前端。 |
| 統一錯誤訊息格式 | 多個微服務合併時,前端需要同一套錯誤結構才能快速處理。 | 在 error_format_middleware 中捕捉所有例外,統一包裝成 {code, message, trace_id},同時把 trace_id 加入日誌。 |
總結
- 例外與 Middleware 是 FastAPI 中不可分割的兩大機制,透過正確的層級劃分,我們可以在 全域日誌、事務管理、統一錯誤格式 上取得最佳平衡。
- 先 定義自訂例外,再 在全域 Exception Handler 中統一回應,最後 在 Middleware 捕捉未處理的例外或執行跨請求的前置/後置工作。
- 避免 重複捕捉、遺漏回應,並善用
request.state共享上下文,能讓程式碼保持 乾淨、可測試、易維護。
掌握了例外與中介層的互動方式,你的 FastAPI 專案將具備 高可觀測性、健壯的錯誤處理與一致的 API 介面,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利 🚀!