在前面的章節中,我們專注於如何「正確」地執行程式,但程式碼也總有出錯的時候,就算是 AI 產出來的程式碼也不一定是 100% 正確。一個穩定的應用程式不僅要能處理正常請求,更要在發生錯誤時做出適當的回應。今天,我們就來探討 FastAPI 中最基礎也最重要的錯誤處理機制。
當你的 API 程式碼中發生一個未被處理的 Python 錯誤時 (例如,TypeError
、ValueError
或存取一個不存在的 dict key),FastAPI 會自動攔截這個例外,並回傳一個 HTTP 500 Internal Server Error
的回應。
這是一個很合理的預設行為,它防止了內部的錯誤堆疊資訊 (stack trace) 直接洩漏給前端使用者,避免了潛在的安全風險。然而,對於開發者來說,一個通用的「500」錯誤訊息並不利於除錯。
重要觀念:錯誤的隔離性
值得注意的是,在 FastAPI 的異步架構下,單個 API 請求中發生的錯誤只會影響該請求本身,不會造成整個 FastAPI 應用程式掛掉。每個請求都在獨立的協程中處理,因此一個請求的錯誤不會波及到其他並發的請求。這種隔離性是 FastAPI 穩定性的重要特性之一。
當然,這個前提是你沒有在 API 中額外建立新的 process 或 thread —— 那些情況下的錯誤處理會更複雜,我們會在後續章節中討論。
HTTPException
: 主動拋出 API 錯誤在多數情況下,我們會遇到一些「預期中的錯誤」,例如使用者請求一個不存在的資料、傳入的參數格式不符等等。在這種情況下,回傳 500 錯誤並不合適,我們應該回傳更精確的 HTTP 狀態碼,如 404 Not Found
或 400 Bad Request
。
FastAPI 提供了 HTTPException
讓我們可以輕鬆做到這點。
from fastapi import FastAPI, HTTPException
app = FastAPI()
items_db = {"item1": "Apple", "item2": "Banana"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items_db:
# 當 item_id 不存在時,拋出 HTTPException
# FastAPI 會自動將其轉換為標準的 HTTP 錯誤回應
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items_db[item_id]}
在上面的範例中,如果使用者請求一個不存在的 item_id
,他們會收到一個 JSON 回應,內容為 {"detail": "Item not found"}
,並且 HTTP 狀態碼為 404。這對 API 的使用者來說,是非常清晰明瞭的。
(這裡用的是 VSCode 套件版的 Postman)
當錯誤發生時,除了回傳給使用者,我們更重要的任務是「紀錄」它,以便後續分析與修正。雖然我們可以在每個發生錯誤的地方都寫一行 log,但這顯然不夠優雅。最好的方式是透過 Middleware 來建立一個統一的錯誤日誌紀錄層。
Middleware 像一個洋蔥層,包裹著我們的 API 路由。我們可以在這層攔截所有未被處理的例外,進行紀錄,然後再回傳一個標準的錯誤回應。
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
# 設定日誌
logger = logging.getLogger("uvicorn")
app = FastAPI()
@app.middleware("http")
async def log_errors_middleware(request: Request, call_next):
try:
# 嘗試處理請求
response = await call_next(request)
return response
except Exception as e:
# 捕捉到任何繼承自 Exception 的錯誤
logger.error(f"Unhandled error: {e}")
# 你可以在這裡回傳一個通用的錯誤訊息
return JSONResponse(
status_code=500,
content={"message": "Internal Server Error"},
)
@app.get("/divide/{a}/{b}")
async def divide(a: int, b: int):
# 這個路由可能會觸發 ZeroDivisionError
return {"result": a / b}
在這個範例中,我們建立了一個 log_errors_middleware
。try
區塊會正常執行 API 邏輯。如果 API 內部(例如 divide
函式)拋出了任何未被處理的錯誤(例如 ZeroDivisionError
),except
區塊就會被觸發。
在這裡,我們使用 logger.error
紀錄錯誤資訊(如果需要詳細的錯誤資訊,可以加上 exc_info=True
,就會包含完整的 stack trace),然後回傳一個標準的 500 錯誤給使用者,從而完美地達成了我們的目標:對內紀錄詳細錯誤,對外回傳通用訊息。
如果有設定
exc_info=True
,會看到很長的錯誤資訊,就不放截圖上來了XD
今天我們學習了 FastAPI 錯誤處理的基礎。明天,我們將深入探討一個更複雜的情境:當錯誤發生在背景任務或其他 thread 時,我們該如何優雅地處理它們,並確保我們的應用程式能穩定運行。