本文 AI 產出,尚未審核

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

說明

  1. call_next 代表 下一層(路由或其他 Middleware)的處理結果。
  2. call_next 期間拋出例外,Middleware 會 攔截 並自行產生回應,此時 之後的 Exception Handler 不會再被觸發
  3. 透過 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 無回應錯誤。 捕捉例外後 一定要回傳 JSONResponsePlainTextResponse 或自訂 Response
使用 raise HTTPException 於非 HTTP 層 在非請求上下文(如背景任務)拋出 HTTPException,FastAPI 無法自動轉換。 在背景任務中使用自訂例外,或自行呼叫 JSONResponse
過度依賴全域 Exception Handler 把所有錯誤都交給全域處理器,可能隱藏業務邏輯錯誤。 針對不同錯誤類型 建立 專屬的 handler(如驗證、授權、資料庫),保持錯誤資訊的語意。
未設定 response_model 即使例外被捕捉,回傳的 JSON 結構仍可能不符合 API 文件。 統一錯誤模型:建立 ErrorResponse Pydantic 模型,讓所有例外回傳相同結構。

小技巧

  • 使用 logger.exception():自動把 traceback 寫入日誌,適合在 Middleware 捕捉例外時使用。
  • request.state 中傳遞上下文:如 DB session、用戶資訊,避免在每個路由都重複取得。
  • 測試例外流程:使用 TestClientraise_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 介面,為日後的功能擴充與團隊協作奠定堅實基礎。祝開發順利 🚀!