跳至內容

錯誤處理

在許多情況下,您需要將錯誤通知給使用您 API 的客戶端。

此客戶端可以是帶有前端的瀏覽器、其他人的程式碼、IoT 裝置等等。

您可能需要告知客戶端

  • 客戶端沒有足夠的權限執行該操作。
  • 客戶端無法存取該資源。
  • 客戶端嘗試存取的項目不存在。
  • 等等。

在這些情況下,您通常會返回範圍在 **400**(從 400 到 499)的 **HTTP 狀態碼**。

這類似於 200 HTTP 狀態碼(從 200 到 299)。這些「200」狀態碼表示請求在某種程度上「成功」。

400 範圍內的狀態碼表示客戶端發生錯誤。

還記得所有那些 **「404 Not Found」** 錯誤(和笑話)嗎?

使用 HTTPException

要將包含錯誤的 HTTP 回應返回給客戶端,您可以使用 HTTPException

匯入 HTTPException

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

在您的程式碼中引發 HTTPException

HTTPException 是一個普通的 Python 例外,其中包含與 API 相關的額外數據。

因為它是一個 Python 例外,所以您不 return 它,而是 raise 它。

這也意味著,如果您在*路徑操作函式*內部呼叫的工具函式內部,並且您從該工具函式內部引發 HTTPException,它將不會執行*路徑操作函式*中的其餘程式碼,它將立即終止該請求,並將 HTTPException 中的 HTTP 錯誤傳送給客戶端。

引發例外而不是 return 一個值的優點將在關於依賴項和安全性的章節中更加明顯。

在此範例中,當客戶端透過不存在的 ID 請求項目時,引發狀態碼為 404 的例外。

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

結果回應

如果客戶端請求 http://example.com/items/fooitem_id"foo"),則該客戶端將收到 HTTP 狀態碼 200,以及以下 JSON 回應:

{
  "item": "The Foo Wrestlers"
}

但如果客戶端請求 http://example.com/items/bar(不存在的 item_id "bar"),則該客戶端將收到 HTTP 狀態碼 404(「找不到」錯誤),以及以下 JSON 回應:

{
  "detail": "Item not found"
}

提示

引發 HTTPException 時,您可以將任何可以轉換為 JSON 的值作為參數 detail 傳遞,而不僅僅是 str

您可以傳遞 dictlist 等等。

它們會由 FastAPI 自動處理並轉換為 JSON。

新增自訂標頭

在某些情況下,能夠將自訂標頭新增至 HTTP 錯誤非常有用。例如,對於某些類型的安全性。

您可能不需要直接在程式碼中使用它。

但如果您在進階情境中需要它,您可以新增自訂標頭。

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}

安裝自訂例外處理常式

您可以使用Starlette 的相同例外工具新增自訂例外處理常式。

假設您有一個自訂例外 UnicornException,您(或您使用的函式庫)可能會 raise 它。

您想要在 FastAPI 中全局處理這個例外。

您可以使用 @app.exception_handler() 添加自訂例外處理器。

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


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


app = FastAPI()


@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )


@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name=name)
    return {"unicorn_name": name}

在這裡,如果您請求 /unicorns/yolo,*路徑操作* 將會 raise 一個 UnicornException

但它會被 unicorn_exception_handler 處理。

因此,您會收到一個簡潔的錯誤,HTTP 狀態碼為 418,JSON 內容為

{"message": "Oops! yolo did something. There goes a rainbow..."}

「技術細節」

您也可以使用 from starlette.requests import Requestfrom starlette.responses import JSONResponse

FastAPI 提供與 starlette.responses 相同的 fastapi.responses,只是為了方便您開發。但大多數可用的回應都直接來自 Starlette。Request 也是一樣。

覆寫預設例外處理器

FastAPI 有一些預設的例外處理器。

當您 raise 一個 HTTPException 以及請求具有無效資料時,這些處理器負責返回預設的 JSON 回應。

您可以使用自己的處理器覆寫這些例外處理器。

覆寫請求驗證例外

當請求包含無效資料時,FastAPI 內部會引發 RequestValidationError

它也包含一個預設的例外處理器。

要覆寫它,請匯入 RequestValidationError 並使用 @app.exception_handler(RequestValidationError) 裝飾例外處理器。

例外處理器將會收到一個 Request 和例外。

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

現在,如果您前往 /items/foo,您將會得到一個文字版本的錯誤,而不是預設的 JSON 錯誤,其中包含

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

您會得到一個文字版本,其中包含

1 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationError vs ValidationError

警告

如果您現在覺得不重要,可以跳過這些技術細節。

RequestValidationError 是 Pydantic 的 ValidationError 的子類別。

FastAPI 使用它,以便在您在 response_model 中使用 Pydantic 模型,並且您的資料有錯誤時,您可以在日誌中看到錯誤。

但客戶端/使用者不會看到它。相反地,客戶端會收到一個 HTTP 狀態碼為 500 的「內部伺服器錯誤」。

應該這樣做,因為如果您在 *回應* 中或程式碼中的任何位置(而不是在客戶端的 *請求* 中)出現 Pydantic ValidationError,則實際上是您的程式碼中有錯誤。

在您修復它時,您的客戶端/使用者不應該存取有關錯誤的內部資訊,因為這可能會暴露安全漏洞。

覆寫 HTTPException 錯誤處理器

同樣地,您可以覆寫 HTTPException 處理器。

例如,您可能希望為這些錯誤返回純文字回應,而不是 JSON

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

「技術細節」

您也可以使用 from starlette.responses import PlainTextResponse

FastAPI 提供與 starlette.responses 相同的 fastapi.responses,只是為了方便您開發。但大多數可用的回應都直接來自 Starlette。

使用 RequestValidationError 主體

RequestValidationError 包含它收到的具有無效資料的 body(主體)。

您可以在開發應用程式時使用它來記錄主體並進行除錯、將其返回給使用者等。

from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )


class Item(BaseModel):
    title: str
    size: int


@app.post("/items/")
async def create_item(item: Item):
    return item

現在嘗試傳送一個無效的項目,例如

{
  "title": "towel",
  "size": "XL"
}

您會收到一個回應,告訴您資料無效,其中包含收到的主體

{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

FastAPI 的 HTTPException vs Starlette 的 HTTPException

FastAPI 有自己的 HTTPException

而且 FastAPIHTTPException 錯誤類別繼承自 Starlette 的 HTTPException 錯誤類別。

唯一的差別在於 FastAPIHTTPExceptiondetail 欄位接受任何可 JSON 化的資料,而 Starlette 的 HTTPException 只接受字串。

所以,您可以在程式碼中照常引發 FastAPIHTTPException

但是當您註冊例外處理器時,您應該將它註冊給 Starlette 的 HTTPException

這樣一來,如果 Starlette 的任何內部程式碼、Starlette 的擴充套件或外掛程式引發了 Starlette 的 HTTPException,您的處理器就能夠捕捉並處理它。

在此範例中,為了能夠在同一個程式碼中同時使用兩種 HTTPException,Starlette 的例外被重新命名為 StarletteHTTPException

from starlette.exceptions import HTTPException as StarletteHTTPException

重複使用 FastAPI 的例外處理器

如果您想使用這個例外,並同時使用 FastAPI 中相同的預設例外處理器,您可以從 fastapi.exception_handlers 導入並重複使用預設的例外處理器。

from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
    print(f"OMG! An HTTP error!: {repr(exc)}")
    return await http_exception_handler(request, exc)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    print(f"OMG! The client sent invalid data!: {exc}")
    return await request_validation_exception_handler(request, exc)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

在此範例中,您只是使用非常具體的訊息 print 錯誤,但您應該理解這個概念。您可以使用這個例外,然後重複使用預設的例外處理器。