FastAPI 教學:全域錯誤攔截 (Exception Handling)
簡介
在 Web API 開發中,錯誤處理 是不可或缺的環節。若錯誤資訊直接暴露給前端或使用者,不僅會影響使用體驗,也可能洩漏系統內部細節,造成安全風險。FastAPI 提供了強大的例外攔截機制,讓開發者能以統一且可自訂的方式回應錯誤訊息。
本單元聚焦於 全域錯誤攔截(Global Exception Handling),說明如何在 FastAPI 應用程式層面捕捉未處理的例外,並回傳結構化、友善的 JSON 錯誤回應。無論是開發階段的除錯,或是上線後的錯誤監控,掌握全域錯誤攔截都是提升 API 品質的關鍵。
核心概念
1. 為什麼需要全域錯誤攔截?
- 統一回應格式:客戶端只要依賴同一套錯誤結構,就能簡化前端的錯誤處理邏輯。
- 隱藏敏感資訊:避免把原始例外訊息直接回傳,降低資訊洩漏的風險。
- 集中日誌與監控:所有未捕獲的例外都會走同一條路徑,方便寫入日誌或送至外部監控平台(如 Sentry、Datadog)。
FastAPI 內建的 ExceptionHandler 讓我們可以在應用層註冊自訂的例外處理器,實現上述需求。
2. 基本的全域例外處理器
FastAPI 允許使用 app.exception_handler() 裝飾器為 任意例外類別 設定處理器。最常見的做法是針對 Exception(所有未捕獲的例外)建立一個全域捕捉。
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import traceback
app = FastAPI()
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""
捕捉所有未處理的例外,回傳統一的 JSON 結構。
"""
# 1️⃣ 取得例外的完整堆疊資訊(僅在開發環境使用)
tb = traceback.format_exc()
# 2️⃣ 組合錯誤回應內容
error_response = {
"error": {
"type": exc.__class__.__name__,
"message": str(exc),
# 開發階段可加入 stacktrace,正式環境可省略
"stacktrace": tb if app.debug else None,
},
"request": {
"method": request.method,
"url": str(request.url),
},
}
# 3️⃣ 回傳 500 Internal Server Error
return JSONResponse(status_code=500, content=error_response)
重點:
app.debug(或自行設定的環境變數)決定是否把stacktrace暴露給前端,務必在正式環境關閉。
3. 處理 HTTPException
FastAPI 的 HTTPException 已經會自動產生 JSON 回應,但在全域攔截時,我們仍可以自訂格式,使所有錯誤(包括 404、400 等)保持一致。
from fastapi import HTTPException
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
統一 HTTPException 的回應格式。
"""
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"type": "HTTPException",
"status_code": exc.status_code,
"detail": exc.detail,
},
"request": {
"method": request.method,
"url": str(request.url),
},
},
)
技巧:若想保留 FastAPI 原生的
detail結構,只要把exc.detail直接塞入即可。
4. 自訂業務例外
在大型專案中,我們常會定義自己的例外類別(例如 UserNotFoundError、InsufficientBalanceError),以便在服務層拋出有意義的錯誤。只要把這些類別繼承自 Exception,就會被前面的全域處理器捕獲;若想給予更精細的 HTTP 狀態碼,可在 Exception 子類別中加入 status_code 屬性,然後在全域處理器裡根據它回傳。
class BusinessException(Exception):
"""所有業務例外的基底類別,預設回傳 400 Bad Request。"""
status_code: int = 400
def __init__(self, message: str):
super().__init__(message)
class UserNotFoundError(BusinessException):
status_code = 404
def __init__(self, user_id: int):
super().__init__(f"使用者 ID {user_id} 不存在")
# 在路由中拋出例外
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# 假設查詢失敗
raise UserNotFoundError(user_id)
全域處理器只需要再加一次判斷即可:
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"type": exc.__class__.__name__,
"message": str(exc),
},
"request": {
"method": request.method,
"url": str(request.url),
},
},
)
5. 整合第三方日誌與監控(以 Sentry 為例)
在實務上,我們往往會把未捕獲的例外送到外部監控平台,以便即時警報與事後分析。以下示範如何在全域處理器中加入 Sentry 的報告:
import sentry_sdk
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
# 初始化 Sentry(只在正式環境啟用)
sentry_sdk.init(
dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
environment="production",
traces_sample_rate=0.2,
)
# 把 Sentry 中介層掛載到 FastAPI
app.add_middleware(SentryAsgiMiddleware)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 把例外送到 Sentry
sentry_sdk.capture_exception(exc)
# 其餘回應邏輯同前述
return JSONResponse(
status_code=500,
content={
"error": {"type": exc.__class__.__name__, "message": "系統內部錯誤"},
"request": {"method": request.method, "url": str(request.url)},
},
)
注意:在開發環境可以把
environment="development",避免過多噪音。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 直接回傳原始例外訊息 | 會洩漏程式碼路徑、資料庫結構等資訊。 | 只回傳 自訂的錯誤訊息,如 "系統內部錯誤",堆疊資訊僅寫入日誌。 |
忘記在 Exception 處理器內呼叫 await request.body() |
若例外發生在讀取請求體之後,可能導致無法取得完整的 request 資訊。 | 必要時使用 await request.body() 取得原始 payload,或在 middleware 中捕獲。 |
| 在全域處理器內再次拋出例外 | 會形成遞迴,最終造成 500 內部錯誤無法回傳。 | 確保處理器本身 不會產生新的例外,如使用 try/except 包住日誌寫入程式碼。 |
| 未考慮非同步例外 | 某些背景任務(BackgroundTasks)拋出的例外不會走 FastAPI 的路由層。 |
為背景任務自行加上 try/except,或使用 anyio 的 TaskGroup 監控。 |
在測試環境忘記關閉 debug |
測試時過度暴露 stacktrace,可能造成 CI 日誌過大。 | 透過環境變數或 app.debug = False 控制。 |
最佳實踐
統一錯誤模型
建議所有回應都遵循同一個 JSON schema,例如:{ "error": { "type": "ExceptionClassName", "code": 1234, "message": "說明文字", "detail": {...} }, "request": { "method": "...", "url": "..." } }分層處理
- 路由層:只捕捉與業務相關的例外(如
BusinessException)。 - 全域層:捕捉所有未處理的系統例外,回傳 500。
- 中介層(Middleware):可在此做 日誌、追蹤,避免在每個處理器內重複寫程式。
- 路由層:只捕捉與業務相關的例外(如
環境切換
使用os.getenv("ENV")或pydantic.BaseSettings來決定是否顯示 stacktrace、是否啟用 Sentry。自訂 HTTP 狀態碼
若業務例外需要不同的 HTTP 狀態碼,直接在例外類別裡加入status_code屬性,並在處理器中使用。
實際應用場景
1. 電商平台的訂單 API
在訂單建立流程中,可能會發生以下錯誤:
| 錯誤類型 | 例外 | HTTP 狀態碼 |
|---|---|---|
| 商品庫存不足 | InsufficientStockError (自訂) |
409 Conflict |
| 使用者未登入 | AuthenticationError (自訂) |
401 Unauthorized |
| 系統錯誤(資料庫斷線) | Exception |
500 Internal Server Error |
透過全域錯誤攔截,我們可以保證 所有錯誤回傳格式一致,前端只要根據 error.type 或 error.code 做對應即可。
class InsufficientStockError(BusinessException):
status_code = 409
def __init__(self, product_id: int, requested: int, available: int):
super().__init__(
f"商品 {product_id} 庫存不足:要求 {requested},僅剩 {available}"
)
2. 多服務微服務架構
在微服務環境下,各服務之間的錯誤往往需要 傳遞 給呼叫端。例如,支付服務回傳 PaymentGatewayError,我們可以在 API Gateway 使用全域處理器把它轉成標準化的錯誤回應,並同時在日誌中紀錄跨服務追蹤 ID(如 X-Request-ID)。
@app.exception_handler(PaymentGatewayError)
async def payment_error_handler(request: Request, exc: PaymentGatewayError):
# 取得跨服務追蹤 ID
trace_id = request.headers.get("X-Request-ID", "unknown")
# 寫入集中日誌
logger.error(f"[{trace_id}] Payment error: {exc}")
return JSONResponse(
status_code=502,
content={
"error": {
"type": "PaymentGatewayError",
"message": "第三方支付服務暫時無法使用,請稍後再試",
"trace_id": trace_id,
},
"request": {"method": request.method, "url": str(request.url)},
},
)
3. API 文件與前端協議
使用 OpenAPI 產生的文件時,若全域錯誤回應的 schema 與文件不符,前端會無法正確解析。解決方法是把全域錯誤模型加到 app.openapi_schema 中:
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="My Store API",
version="1.0.0",
routes=app.routes,
)
# 加入全域錯誤的 schema
error_schema = {
"title": "ErrorResponse",
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"type": {"type": "string"},
"code": {"type": "integer"},
"message": {"type": "string"},
},
"required": ["type", "message"],
},
"request": {"type": "object"},
},
"required": ["error"],
}
openapi_schema["components"]["schemas"]["ErrorResponse"] = error_schema
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
如此一來,Swagger UI 會自動顯示錯誤回應的結構,讓前端開發者一目了然。
總結
全域錯誤攔截是 FastAPI 應用程式不可或缺的基礎建設。透過 app.exception_handler() 我們可以:
- 統一錯誤回應,讓前端只需依賴單一 JSON schema 即可處理所有錯誤。
- 隱藏敏感資訊,僅在開發環境提供 stacktrace,正式環境回傳簡潔訊息。
- 集中日誌與監控,結合 Sentry、Datadog 等第三方服務,實現即時告警與事後分析。
- 支援自訂業務例外,讓服務層拋出的錯誤可以直接映射到合適的 HTTP 狀態碼與訊息。
在實務開發中,建議遵循以下三點:
- 先設計錯誤模型,再實作全域處理器。
- 分層捕捉:路由層處理業務例外,全域層捕捉系統例外。
- 環境感知:根據
debug、ENV等變數決定是否顯示堆疊或啟用外部監控。
掌握了全域錯誤攔截,你的 FastAPI 專案將更安全、可維護、且易於與前端團隊協作。祝開發順利,API 穩定上線! 🚀