iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

在前面的章節中,我們專注於如何「正確」地執行程式,但程式碼也總有出錯的時候,就算是 AI 產出來的程式碼也不一定是 100% 正確。一個穩定的應用程式不僅要能處理正常請求,更要在發生錯誤時做出適當的回應。今天,我們就來探討 FastAPI 中最基礎也最重要的錯誤處理機制。

FastAPI 的預設錯誤處理

當你的 API 程式碼中發生一個未被處理的 Python 錯誤時 (例如,TypeErrorValueError 或存取一個不存在的 dict key),FastAPI 會自動攔截這個例外,並回傳一個 HTTP 500 Internal Server Error 的回應。

這是一個很合理的預設行為,它防止了內部的錯誤堆疊資訊 (stack trace) 直接洩漏給前端使用者,避免了潛在的安全風險。然而,對於開發者來說,一個通用的「500」錯誤訊息並不利於除錯。

重要觀念:錯誤的隔離性

值得注意的是,在 FastAPI 的異步架構下,單個 API 請求中發生的錯誤只會影響該請求本身,不會造成整個 FastAPI 應用程式掛掉。每個請求都在獨立的協程中處理,因此一個請求的錯誤不會波及到其他並發的請求。這種隔離性是 FastAPI 穩定性的重要特性之一。

當然,這個前提是你沒有在 API 中額外建立新的 process 或 thread —— 那些情況下的錯誤處理會更複雜,我們會在後續章節中討論。

HTTPException: 主動拋出 API 錯誤

在多數情況下,我們會遇到一些「預期中的錯誤」,例如使用者請求一個不存在的資料、傳入的參數格式不符等等。在這種情況下,回傳 500 錯誤並不合適,我們應該回傳更精確的 HTTP 狀態碼,如 404 Not Found400 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)

使用 Middleware 統一紀錄錯誤

當錯誤發生時,除了回傳給使用者,我們更重要的任務是「紀錄」它,以便後續分析與修正。雖然我們可以在每個發生錯誤的地方都寫一行 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_middlewaretry 區塊會正常執行 API 邏輯。如果 API 內部(例如 divide 函式)拋出了任何未被處理的錯誤(例如 ZeroDivisionError),except 區塊就會被觸發。

在這裡,我們使用 logger.error 紀錄錯誤資訊(如果需要詳細的錯誤資訊,可以加上 exc_info=True,就會包含完整的 stack trace),然後回傳一個標準的 500 錯誤給使用者,從而完美地達成了我們的目標:對內紀錄詳細錯誤,對外回傳通用訊息。

如果有設定 exc_info=True,會看到很長的錯誤資訊,就不放截圖上來了XD

小結

今天我們學習了 FastAPI 錯誤處理的基礎。明天,我們將深入探討一個更複雜的情境:當錯誤發生在背景任務或其他 thread 時,我們該如何優雅地處理它們,並確保我們的應用程式能穩定運行。


上一篇
[Day 18] 任務管理 (四):任務佇列範例
下一篇
[Day 20] 錯誤處理 (二):進階
系列文
用 FastAPI 打造你的 AI 服務22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言