本文 AI 產出,尚未審核
FastAPI
例外與錯誤處理(Exception Handling)
主題:try / except 處理例外
簡介
在 Web API 開發中,**例外(Exception)**是不可避免的。無論是使用者輸入錯誤、資料庫連線失敗,或是第三方服務回傳非預期資料,都會拋出例外。如果不適當地捕獲與回應,整個服務可能直接回傳 500 Internal Server Error,讓前端難以取得有意義的錯誤資訊,也會造成除錯成本飆升。
FastAPI 內建與 Python 完全相容的 try / except 機制,同時提供了 HTTPException、全域例外處理器等工具,讓開發者可以把例外轉換成符合 REST 標準的回應。掌握正確的例外處理方式,不僅能提升 API 的可用性與安全性,還能在日後維護與擴充時減少意外錯誤的衝擊。
本篇文章將以 try / except 為核心,逐步說明在 FastAPI 中如何捕獲、轉換與統一管理例外,並提供實務範例、常見陷阱與最佳實踐,幫助你從「會寫」到「寫得好」。
核心概念
1. 基本的 try / except 結構
from fastapi import FastAPI
app = FastAPI()
@app.get("/divide")
def divide(a: int, b: int):
try:
result = a / b # 可能拋出 ZeroDivisionError
return {"result": result}
except ZeroDivisionError as e:
# 把 Python 例外轉成 HTTP 回應
return {"error": "除數不能為 0", "detail": str(e)}
- 重點:
except只捕獲我們預期會發生的例外,避免把所有錯誤都吞掉。
2. 捕獲多種例外與使用 as 取得例外資訊
@app.get("/read-file")
def read_file(path: str):
try:
with open(path, "r", encoding="utf-8") as f:
return {"content": f.read()}
except (FileNotFoundError, PermissionError) as exc:
# 兩種例外共用同一段處理邏輯
return {"error": "無法讀取檔案", "detail": str(exc)}
- 技巧:將同類型的例外放在同一個 tuple 中,讓程式碼更簡潔。
3. 使用 FastAPI 的 HTTPException
HTTPException 會自動被 FastAPI 轉成符合 HTTP 標準的回應(包括 status code、detail 等),是最常見的錯誤回傳方式。
from fastapi import HTTPException, status
@app.get("/item/{item_id}")
def get_item(item_id: int):
try:
item = database.get(item_id) # 假設會拋出 KeyError
return {"item": item}
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item {item_id} 不存在"
)
- 優點:前端只需要根據 HTTP 狀態碼判斷錯誤類型,無需自行解析自訂的錯誤結構。
4. 自訂例外類別
在大型專案中,常會建立一套自訂例外,以統一錯誤代碼與訊息。
class BusinessError(Exception):
"""業務邏輯錯誤,會被全域處理器捕獲"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
@app.post("/order")
def create_order(order: dict):
if order["quantity"] <= 0:
raise BusinessError(1001, "訂購數量必須大於 0")
# 正常處理流程...
return {"status": "ok"}
- 說明:自訂例外只負責「拋出」資訊,真正的回應格式交給全域例外處理器決定。
5. 全域例外處理器(Exception Handler)
FastAPI 允許我們為特定例外類別註冊統一的處理器,讓所有拋出的例外都能得到一致的回應格式與日誌。
from fastapi.responses import JSONResponse
from fastapi.exception_handlers import http_exception_handler
@app.exception_handler(BusinessError)
async def business_error_handler(request, exc: BusinessError):
# 這裡可以寫入日誌、上報監控系統等
return JSONResponse(
status_code=400,
content={
"error_code": exc.code,
"error_message": exc.message,
"detail": str(exc)
},
)
# 仍保留 FastAPI 內建的 HTTPException 處理
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
return await http_exception_handler(request, exc)
- 好處:
- 統一回應格式:前端只要依照
error_code、error_message解析即可。 - 集中日誌:所有業務錯誤一次寫入,避免散落在各個 endpoint。
- 統一回應格式:前端只要依照
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 推薦做法 |
|---|---|---|
使用裸 except: 捕獲所有例外 |
真正的程式錯誤被隱藏,難以排除 | 只捕獲需要的例外(如 except ValueError:) |
在 except 區塊內再次拋出未處理的例外 |
會變成 500 錯誤,使用者看不到友善訊息 | 使用 raise 重新拋出自訂例外或 HTTPException |
在非同步 (async) 路由中混用同步阻塞操作 |
會阻塞事件迴圈,降低吞吐量 | 使用 await 或 run_in_threadpool |
| 忽略例外訊息的日誌記錄 | 事後排查困難 | 在全域例外處理器中寫入結構化日誌 |
把業務邏輯的錯誤直接回傳 detail 給前端 |
可能洩漏內部實作或資料庫資訊 | 只回傳必要的錯誤代碼與訊息 |
最佳實踐
- 分層捕獲:在服務層(service)拋出自訂例外,在 API 層只捕獲
BusinessError或HTTPException。 - 統一錯誤模型:如
{ "error_code": int, "error_message": str, "detail": str },讓前端 SDK 能自動映射。 - 結構化日誌:使用
loguru、structlog等套件,把例外類別、路徑、使用者 ID 等資訊寫入。 - 測試例外路徑:利用
pytest的client.get(..., raise_server_exceptions=False)確認回傳的錯誤碼與內容。 - 避免在
except內做大量運算:保持例外處理的輕量,重的邏輯應移至服務層或背景工作。
實際應用場景
1. 使用者註冊時的資料驗證
@app.post("/register")
def register_user(payload: dict):
try:
email = payload["email"]
if not re.match(r".+@.+\..+", email):
raise BusinessError(2001, "Email 格式不正確")
# 假設下面的 DB 操作會拋出 IntegrityError
db.create_user(email, payload["password"])
return {"status": "registered"}
except BusinessError as be:
raise HTTPException(status_code=400, detail=be.message)
except IntegrityError:
raise HTTPException(status_code=409, detail="Email 已被使用")
- 說明:先在業務層拋出自訂例外,最後統一轉成
HTTPException,前端只會得到 400 或 409。
2. 呼叫第三方支付 API
import httpx
@app.post("/pay")
async def pay(order_id: str):
try:
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://payment.example.com/api/pay",
json={"order_id": order_id},
timeout=5.0,
)
resp.raise_for_status() # 若非 2xx 會拋出 HTTPStatusError
data = resp.json()
if data["status"] != "ok":
raise BusinessError(3001, "支付失敗:" + data["msg"])
return {"payment_id": data["payment_id"]}
except httpx.HTTPStatusError as e:
raise HTTPException(
status_code=502,
detail=f"第三方服務回傳錯誤:{e.response.status_code}"
)
except httpx.RequestError as e:
raise HTTPException(
status_code=504,
detail=f"連線第三方服務失敗:{str(e)}"
)
- 要點:把外部服務的例外轉成 5xx,讓監控系統能辨識「外部依賴」的失效。
3. 資料庫交易失敗的回滾
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
@app.put("/profile/{user_id}")
def update_profile(user_id: int, profile: dict, db: Session = Depends(get_db)):
try:
user = db.query(User).filter(User.id == user_id).one()
for k, v in profile.items():
setattr(user, k, v)
db.commit()
return {"status": "updated"}
except SQLAlchemyError as e:
db.rollback() # 必須手動回滾
raise HTTPException(status_code=500, detail="資料庫錯誤,已回滾")
- 提醒:在捕獲資料庫例外後,務必回滾交易,否則連線會進入不一致狀態。
總結
try / except是 Python 與 FastAPI 處理例外的基礎,只捕獲需要的例外,避免濫用except:。- 透過
HTTPException、自訂例外與 全域例外處理器,可以把內部錯誤安全且一致地呈現給前端。 - 統一錯誤模型、結構化日誌、測試例外路徑 是提升服務可觀測性與維護性的關鍵。
- 在實務開發中,將 業務錯誤、外部服務錯誤、資料庫錯誤 分別以不同的 HTTP 狀態碼與錯誤代碼回傳,讓前端與監控系統都能快速定位問題。
掌握以上技巧後,你的 FastAPI 專案將能在 錯誤發生時仍保持可預測、易除錯,且不會洩漏敏感資訊,為使用者提供更可靠的 API 服務。祝開發順利!