本文 AI 產出,尚未審核

FastAPI 教學:例外與錯誤處理 – RequestValidationError 處理

簡介

在使用 FastAPI 建立 RESTful API 時,最常見的錯誤之一就是 RequestValidationError。它會在請求的資料(query、path、body、header 等)無法通過 Pydantic 模型驗證時被拋出。若不加以處理,使用者會直接收到 FastAPI 預設的 422 回應,內容雖然包含錯誤細節,但格式較為機械,且不易客製化。

適當地捕捉與轉換 RequestValidationError,不僅可以提供 更友善的錯誤訊息,還能讓前端團隊統一錯誤回傳結構、加上錯誤代碼或紀錄日誌,提升 API 的可維護性與使用者體驗。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 RequestValidationError 的處理方式。


核心概念

1. 為什麼會產生 RequestValidationError

FastAPI 依賴 Pydantic 來定義資料模型,當請求進來時,框架會自動把原始 JSON、query string、path 參數等轉換成對應的 Pydantic 物件。若資料型別、必填欄位、值的範圍等驗證失敗,就會拋出 fastapi.exceptions.RequestValidationError

2. 預設的錯誤回應

{
  "detail": [
    {
      "loc": ["body", "username"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

這樣的結構對開發者友好,但對最終使用者或前端團隊來說,缺少統一的錯誤代碼與說明

3. 自訂例外處理器

FastAPI 允許使用 app.exception_handler() 來註冊自訂的例外處理函式。只要捕捉到 RequestValidationError,就能把原始的 detail 重新包裝成自己想要的格式。

4. 例外處理的執行順序

  • 路由層(dependency)Pydantic 驗證例外處理器
    若在路由的依賴(dependency)中自行拋出 HTTPException,會在驗證之前被捕捉;但 RequestValidationError 必定在 Pydantic 完成驗證之後才會觸發

程式碼範例

以下示範 5 個常見且實用的 RequestValidationError 處理方式,皆以 Python 為例(語法標記使用 python)。

範例 1:最簡單的全局處理器

把 422 回應改寫成我們自己的 JSON 結構,包含錯誤代碼 ERR_VALIDATION

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    # 把原始的 detail 直接回傳,並加上自訂欄位
    return JSONResponse(
        status_code=422,
        content={
            "error_code": "ERR_VALIDATION",
            "message": "請求參數驗證失敗",
            "details": exc.errors(),          # 直接使用 Pydantic 提供的錯誤列表
            "path": request.url.path,
        },
    )

重點exc.errors() 會回傳一個結構化的錯誤列表,比 exc.detail 更易於前端解析。


範例 2:將錯誤訊息轉成「欄位 → 錯誤」的字典

前端常需要把每個欄位的錯誤顯示在對應的表單欄位上,下面的處理器會把錯誤整理成 field: message 的形式。

@app.exception_handler(RequestValidationError)
async def pretty_validation_error(request: Request, exc: RequestValidationError):
    field_errors = {}
    for err in exc.errors():
        # loc 例: ["body", "username"] 或 ["query", "page"]
        field = ".".join(str(loc) for loc in err["loc"] if isinstance(loc, str))
        field_errors[field] = err["msg"]

    return JSONResponse(
        status_code=422,
        content={
            "error_code": "VALIDATION_ERROR",
            "message": "參數格式錯誤",
            "errors": field_errors,
        },
    )

範例 3:加入日誌與 Sentry 追蹤

在正式環境中,將驗證失敗寫入日誌或上報至錯誤追蹤系統是必要的。下面示範如何同時回傳自訂錯誤與寫入日誌。

import logging
import sentry_sdk

logger = logging.getLogger("uvicorn.error")

@app.exception_handler(RequestValidationError)
async def logged_validation_error(request: Request, exc: RequestValidationError):
    # 記錄錯誤
    logger.warning(
        "Validation error on %s: %s",
        request.url.path,
        exc.errors()
    )
    # 上報至 Sentry(若已初始化)
    sentry_sdk.capture_exception(exc)

    return JSONResponse(
        status_code=422,
        content={
            "error_code": "VALIDATION_ERROR",
            "message": "請檢查提交的資料",
            "details": exc.errors(),
        },
    )

範例 4:針對不同來源(body、query、path)回傳不同錯誤代碼

@app.exception_handler(RequestValidationError)
async def differentiated_error(request: Request, exc: RequestValidationError):
    body_errors = []
    query_errors = []
    path_errors = []

    for err in exc.errors():
        loc = err["loc"]
        if "body" in loc:
            body_errors.append(err)
        elif "query" in loc:
            query_errors.append(err)
        elif "path" in loc:
            path_errors.append(err)

    return JSONResponse(
        status_code=422,
        content={
            "error_code": "VALIDATION_ERROR",
            "message": "參數驗證失敗",
            "body_errors": body_errors,
            "query_errors": query_errors,
            "path_errors": path_errors,
        },
    )

技巧:前端只需要顯示 body_errors,而 query_errors 可用於自動修正頁碼等情境。


範例 5:結合依賴(Dependency)與自訂例外

有時候我們想在 dependency 中先做一次簡易檢查,若失敗就直接拋出自訂的 HTTPException,並在全局處理器中統一格式化。

from fastapi import Depends, HTTPException, status

def verify_token(token: str = Depends(...)):
    if token != "secret-token":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="無效的驗證令牌"
        )
    return token

@app.get("/protected")
async def protected_route(token: str = Depends(verify_token)):
    return {"msg": "成功取得受保護資源"}

# 針對 HTTPException 的全局處理(可與 RequestValidationError 同時存在)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error_code": "AUTH_ERROR" if exc.status_code == 401 else "HTTP_ERROR",
            "message": exc.detail,
        },
    )

常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記回傳 status_code 只回傳 content 會讓 FastAPI 使用預設 200,前端會誤以為成功。 務必在 JSONResponse 中明確設定 status_code=422(或其他適當的碼)。
直接回傳 exc.detail 會把 FastAPI 原始結構直接暴露,缺少自訂欄位,且未統一錯誤代碼。 自行包裝,加入 error_codemessagetimestamp 等欄位。
在全局處理器裡再次拋出例外 會導致遞迴或錯誤訊息被覆寫,最終回傳 500。 只回傳 JSONResponse,不要在處理器內再拋例外。
未考慮多語系需求 直接寫死中文訊息,無法支援國際化。 使用 i18n 套件(如 fastapi-babel)或自行根據 Accept-Language 產生訊息。
驗證錯誤過於冗長 exc.errors() 可能包含大量欄位,前端只需要關鍵資訊。 過濾或簡化,只保留 locmsg,或自行映射成更易讀的文字。

最佳實踐

  1. 統一錯誤格式:所有例外(包括 HTTPException、自訂例外)都使用同一套 JSON 結構,方便前端統一處理。
  2. 加入錯誤代碼error_code 應該是機器可讀的字串,如 VALIDATION_ERRORAUTH_ERROR,便於前端根據代碼顯示不同 UI。
  3. 記錄關鍵資訊:在日誌或 Sentry 中加入 request.pathrequest.methodexc.errors(),有助於快速定位問題。
  4. 分層處理:先在 dependency 中做簡易檢查,後交給 Pydantic 完整驗證,兩層錯誤分離更易除錯。
  5. 測試覆蓋:寫測試案例驗證錯誤回傳結構,確保在模型變更時不會不小心破壞錯誤格式。

實際應用場景

場景一:前端表單即時驗證

使用者在填寫註冊表單時,若後端回傳:

{
  "error_code": "VALIDATION_ERROR",
  "message": "參數格式錯誤",
  "errors": {
    "body.username": "field required",
    "body.email": "value is not a valid email address"
  }
}

前端只要映射 errors 中的鍵值,即可在對應的輸入框下顯示錯誤訊息,提升使用者體驗。

場景二:API 門戶(API Gateway)統一錯誤碼

在微服務架構下,所有服務的 RequestValidationError 都會被統一轉成:

{
  "code": "ERR_422",
  "msg": "Invalid request parameters",
  "data": null
}

API Gateway 再把此結構直接回傳給呼叫端,讓各服務不必重複實作錯誤格式。

場景三:自動化測試與 CI

在 CI 流程中,使用 pytest 撰寫測試:

def test_invalid_payload(client):
    response = client.post("/items/", json={"price": -5})
    assert response.status_code == 422
    assert response.json()["error_code"] == "VALIDATION_ERROR"

只要錯誤格式保持一致,測試不會因為回傳結構改變而失效。


總結

  • RequestValidationError 是 FastAPI 在資料驗證失敗時拋出的核心例外,若不自行處理會得到預設的 422 回應。
  • 透過 全局例外處理器 (app.exception_handler) 可以將錯誤重新包裝成 統一、易讀、可機器判斷 的 JSON 格式。
  • 常見的實作技巧包括:
    • exc.errors() 轉成 field: message 的字典。
    • 加入日誌、Sentry 追蹤,提升可觀測性。
    • 根據來源(body、query、path)分別回傳錯誤,讓前端更靈活。
  • 注意避免忘記設定 status_code、過度暴露原始錯誤訊息、以及遞迴拋例外等陷阱。
  • 在實務上,統一錯誤結構、加入錯誤代碼、記錄關鍵資訊 是提升 API 可維護性與使用者體驗的關鍵。

透過本文的說明與範例,你應該已經能夠在自己的 FastAPI 專案中,自信且一致地處理 RequestValidationError。祝開發順利,API 更加穩定、好用!