FastAPI 教學:例外與錯誤處理 – HTTPException
簡介
在 Web API 中,錯誤回報 是與前端或第三方服務溝通的關鍵橋樑。若 API 在遭遇參數錯誤、資源不存在或授權失敗時,未能以適當的 HTTP 狀態碼與訊息回應,使用者將無法正確判斷問題所在,甚至可能產生安全風險。
FastAPI 內建的 HTTPException 提供了一個 簡潔且符合標準的方式,讓開發者在程式碼任意位置拋出 HTTP 錯誤,同時自動產生符合 OpenAPI 規範的錯誤文件。掌握 HTTPException 的使用方法,能讓你的 API 更易維護、錯誤回應更一致,也有助於在自動化測試與文件生成時減少不必要的雜訊。
核心概念
1. 為什麼使用 HTTPException
- 符合 RFC 標準:自動設定正確的 status code(如 404、422、500 等)與對應的說明文字。
- 自動產生 JSON 回應:FastAPI 會把例外轉換為
application/json,格式為{ "detail": "錯誤訊息" },符合大多數前端框架的預期。 - 與 OpenAPI 整合:在自動生成的 Swagger UI 中,會顯示每個 endpoint 可能回傳的錯誤狀態與說明,提升 API 可讀性。
2. 基本使用方式
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id == 0:
# 拋出 404 Not Found
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
status_code:必填,指定要回傳的 HTTP 狀態碼。detail:可選,說明文字或任何可 JSON 序列化的資料。headers:可選,傳遞自訂的 HTTP 標頭。
3. 常見的狀態碼與情境
| 狀態碼 | 名稱 | 常見使用情境 |
|---|---|---|
| 400 | Bad Request | 請求參數格式錯誤、驗證失敗 |
| 401 | Unauthorized | 權杖缺失或無效 |
| 403 | Forbidden | 權限不足,使用者無法存取資源 |
| 404 | Not Found | 資源不存在或路徑錯誤 |
| 409 | Conflict | 產生衝突(如唯一鍵違規) |
| 422 | Unprocessable Entity | Pydantic 驗證失敗(FastAPI 自動拋出) |
| 500 | Internal Server Error | 程式內部錯誤,通常不建議直接拋出此碼 |
4. 進階範例
範例 1:自訂錯誤訊息與標頭
@app.get("/protected")
def protected_endpoint(token: str = None):
if token != "secret-token":
raise HTTPException(
status_code=401,
detail="Invalid authentication token",
headers={"WWW-Authenticate": "Bearer"}
)
return {"message": "Access granted"}
重點:
headers參數可以在回應中加入WWW-Authenticate,讓瀏覽器或 API 客戶端知道需要使用何種認證方式。
範例 2:在資料庫操作失敗時拋出 409
from sqlalchemy.exc import IntegrityError
@app.post("/users/")
def create_user(user: UserCreate):
try:
new_user = User(**user.dict())
db.add(new_user)
db.commit()
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=409,
detail="Username already exists"
)
return new_user
說明:當唯一鍵衝突發生時,我們捕捉
IntegrityError,再以HTTPException(409)回傳給前端,讓使用者知道「此帳號已被使用」。
範例 3:使用自訂的錯誤模型
from pydantic import BaseModel
class ErrorResponse(BaseModel):
code: int
message: str
extra: dict | None = None
@app.get("/items/{item_id}", responses={404: {"model": ErrorResponse}})
def get_item(item_id: int):
item = db.query(Item).filter(Item.id == item_id).first()
if not item:
raise HTTPException(
status_code=404,
detail=ErrorResponse(
code=40401,
message="Item not found",
extra={"item_id": item_id}
).dict()
)
return item
技巧:透過
responses參數在路由上宣告自訂錯誤模型,Swagger UI 會顯示完整結構,方便前端開發者了解回傳格式。
範例 4:全局例外處理器結合 HTTPException
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
# 統一錯誤回應格式
return JSONResponse(
status_code=exc.status_code,
content={"error": {"code": exc.status_code, "message": exc.detail}}
)
說明:即使在不同的 endpoint 中多次拋出
HTTPException,只要有此全局處理器,回傳的 JSON 結構就會保持一致,降低前端解析的複雜度。
範例 5:結合 Pydantic 驗證與自訂 HTTPException
from pydantic import BaseModel, validator
class Order(BaseModel):
product_id: int
quantity: int
@validator("quantity")
def quantity_positive(cls, v):
if v <= 0:
raise HTTPException(
status_code=422,
detail="Quantity must be a positive integer"
)
return v
@app.post("/orders/")
def create_order(order: Order):
# 進一步的業務邏輯
return {"status": "created", "order": order}
重點:在 Pydantic 的驗證器裡直接拋出
HTTPException,讓錯誤訊息立即回傳給使用者,而不必在路由函式內額外檢查。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 直接拋出 500 | 只使用 raise HTTPException(500, ...) 會隱藏真實錯誤來源,且不利於除錯。 |
只在確定是不可恢復的系統錯誤時使用,平時應捕捉具體例外(如 IntegrityError、ValueError)後再拋出適當的狀態碼。 |
| detail 資料過大 | 把整個資料庫物件放入 detail 會導致回應過大,甚至洩漏敏感資訊。 |
僅回傳必要的錯誤訊息,或自訂錯誤模型(如上例 ErrorResponse)。 |
忘記設定 headers |
在授權失敗時未加 WWW-Authenticate,前端無法自動觸發認證流程。 |
依照 RFC 7235 於 401/403 回應中加入適當的 WWW-Authenticate 標頭。 |
| 重複寫錯誤回應程式碼 | 每個 endpoint 都自行組裝 JSON,造成維護困難。 | 使用全局例外處理器 (@app.exception_handler) 統一錯誤格式。 |
| 忽略 OpenAPI 文件 | 未在 responses 中宣告自訂錯誤模型,導致 Swagger UI 無法顯示正確的錯誤結構。 |
在路由裝飾器加入 responses={code: {"model": Model}},保持文件與實作同步。 |
最佳實踐
- 盡量使用標準 HTTP 狀態碼:讓使用者(或第三方)能以慣例判斷錯誤類型。
- 錯誤訊息保持可讀且不洩漏內部實作:例如不要回傳 SQL 語句或堆疊追蹤。
- 統一錯誤回應結構:透過全局例外處理器或自訂 BaseModel,讓前端只需寫一次解析程式碼。
- 在 OpenAPI 中明確說明每個錯誤:使用
responses參數描述可能的錯誤碼與模型,提升 API 可用性。 - 測試例外路徑:使用
TestClient撰寫單元測試,確保每個HTTPException都能正確回傳預期的狀態碼與 JSON 結構。
實際應用場景
1. 電子商務平台 – 商品查詢
- 需求:當使用者查詢不存在的商品時,回傳 404,並提供商品 ID 供前端顯示。
- 實作:在查詢函式中使用
raise HTTPException(404, detail={"code": "ITEM_NOT_FOUND", "item_id": id}),並在前端根據code顯示友善訊息。
2. 金融 API – 交易驗證
- 需求:若交易金額超過使用者餘額,必須回傳 403 並說明「餘額不足」。
- 實作:在交易服務層捕捉餘額檢查失敗,拋出
HTTPException(403, detail="Insufficient balance"),同時在全局處理器加上X-Error-Type: BalanceError標頭,方便日誌分析。
3. 多租戶 SaaS – 權限控管
- 需求:不同租戶只能存取自己的資源,未授權存取時回傳 403,並在
WWW-Authenticate中告知需要重新登入。 - 實作:在依賴注入的驗證函式中檢查租戶 ID,若不匹配則
raise HTTPException(403, detail="Access denied", headers={"WWW-Authenticate": "Bearer"})。
4. 公開 API – 限流 (Rate Limiting)
- 需求:當使用者超過每分鐘 100 次請求的上限,回傳 429 Too Many Requests,並在
Retry-After標頭告知可重新請求的時間。 - 實作:在中介層(middleware)統計請求次數,若超限則
raise HTTPException(status_code=429, detail="Rate limit exceeded", headers={"Retry-After": "60"})。
總結
HTTPException 是 FastAPI 提供的 第一線錯誤回報機制,它不僅能讓開發者快速拋出符合 HTTP 標準的錯誤,還能自動與 OpenAPI 文件同步,提升 API 的可讀性與可維護性。
- 透過 正確的狀態碼、清晰的
detail資訊,以及 必要的自訂標頭,可以讓前端與使用者更快定位問題。 - 全局例外處理器 與 自訂錯誤模型 能夠統一回應結構,減少重複程式碼,並在文件中清楚展示每個 endpoint 可能的錯誤。
- 在實務開發中,遵守 最佳實踐(不洩漏內部資訊、保持錯誤訊息可讀、與 OpenAPI 同步)是打造可靠、易用 API 的關鍵。
掌握了 HTTPException 的使用與相關技巧後,你的 FastAPI 專案將能在 錯誤處理 這塊上更加穩健,讓使用者體驗與開發者維護成本同時獲得提升。祝你寫程式愉快,API 設計更上一層樓!