本文 AI 產出,尚未審核

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, ...) 會隱藏真實錯誤來源,且不利於除錯。 只在確定是不可恢復的系統錯誤時使用,平時應捕捉具體例外(如 IntegrityErrorValueError)後再拋出適當的狀態碼。
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}},保持文件與實作同步。

最佳實踐

  1. 盡量使用標準 HTTP 狀態碼:讓使用者(或第三方)能以慣例判斷錯誤類型。
  2. 錯誤訊息保持可讀且不洩漏內部實作:例如不要回傳 SQL 語句或堆疊追蹤。
  3. 統一錯誤回應結構:透過全局例外處理器或自訂 BaseModel,讓前端只需寫一次解析程式碼。
  4. 在 OpenAPI 中明確說明每個錯誤:使用 responses 參數描述可能的錯誤碼與模型,提升 API 可用性。
  5. 測試例外路徑:使用 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 設計更上一層樓!