錯誤處理¶
在許多情況下,你需要通知使用你 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]}
🤓 Other versions and variants
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]}
在程式中 raise 一個 HTTPException¶
HTTPException 是一般的 Python 例外,但包含與 API 相關的附加資料。
因為它是 Python 的例外,你不是 return,而是 raise。
這也表示,如果你在某個工具函式中(該函式被你的「路徑操作函式」呼叫),並在該工具函式裡 raise HTTPException,那麼「路徑操作函式」剩下的程式碼將不會執行;該請求會立刻被終止,並將 HTTPException 的 HTTP 錯誤傳回給用戶端。
為何選擇 raise 例外而非回傳值的好處,會在相依性與安全性章節更為明顯。
在這個範例中,當用戶端以不存在的 ID 請求項目時,raise 一個狀態碼為 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]}
🤓 Other versions and variants
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/foo(item_id 為 "foo"),會收到 200 的 HTTP 狀態碼,以及以下 JSON 回應:
{
"item": "The Foo Wrestlers"
}
但如果用戶端請求 http://example.com/items/bar(不存在的 item_id "bar"),會收到 404("not found")的 HTTP 狀態碼,以及以下 JSON 回應:
{
"detail": "Item not found"
}
Tip
在 raise 一個 HTTPException 時,你可以將任何可轉為 JSON 的值作為 detail 參數,不只限於 str。
你可以傳入 dict、list 等。
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]}
🤓 Other versions and variants
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}
🤓 Other versions and variants
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 Request 與 from starlette.responses import JSONResponse。
FastAPI 以便利性為由,提供和 starlette.responses 相同的介面於 fastapi.responses。但大多數可用的回應類型其實直接來自 Starlette。Request 也一樣。
覆寫預設例外處理器¶
FastAPI 內建了一些預設例外處理器。
這些處理器負責在你 raise 一個 HTTPException 或請求帶有無效資料時,回傳預設的 JSON 回應。
你可以用自己的處理器來覆寫它們。
覆寫請求驗證例外¶
當請求包含無效資料時,FastAPI 會在內部 raise 一個 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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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}
🤓 Other versions and variants
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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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"
}
]
}
你將會改而得到文字版:
Validation errors:
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
覆寫 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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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}
🤓 Other versions and variants
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: RequestValidationError):
message = "Validation errors:"
for error in exc.errors():
message += f"\nField: {error['loc']}, Error: {error['msg']}"
return PlainTextResponse(message, 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。
Warning
請注意,RequestValidationError 內含驗證錯誤發生的檔名與行號,如果你願意,可以在日誌中顯示這些相關資訊。
但這也代表如果你只是把它轉成字串並直接回傳,可能會洩漏一些關於你系統的資訊。因此這裡的程式碼會分別擷取並顯示每個錯誤。
使用 RequestValidationError 的 body¶
RequestValidationError 包含它收到的(但無效的)body。
在開發應用時,你可以用它來記錄 body 並除錯、回傳給使用者等。
from fastapi import FastAPI, Request
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=422,
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
🤓 Other versions and variants
from fastapi import FastAPI, Request
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=422,
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"
}
你會收到一個告知資料無效、且包含所收到 body 的回應:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
FastAPI 的 HTTPException 與 Starlette 的 HTTPException¶
FastAPI 有自己定義的 HTTPException。
而 FastAPI 的 HTTPException 錯誤類別是繼承自 Starlette 的 HTTPException 錯誤類別。
唯一的差異是,FastAPI 的 HTTPException 在 detail 欄位接受任何可轉為 JSON 的資料,而 Starlette 的 HTTPException 只接受字串。
因此,在你的程式碼中,你可以一如往常地 raise FastAPI 的 HTTPException。
但當你註冊例外處理器時,應該針對 Starlette 的 HTTPException 來註冊。
如此一來,如果 Starlette 的內部程式碼,或任何 Starlette 擴充/外掛 raise 了 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}
🤓 Other versions and variants
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}
在這個範例中,你只是用一段很生動的訊息把錯誤印出來,不過重點是:你可以使用該例外,然後直接重用預設的例外處理器。