前言
許多情況下,需要向客戶端返回一些特定的錯誤,比如
- 客戶端沒有足夠的權限進行該操作
- 客戶端無權訪問該資源
- 客戶端嘗試訪問的項目不存在
HTTPException
介紹
- 要將帶有錯誤的 HTTP 響應(狀態碼和響應信息)返回給客戶端,需要使用 HTTPException
- HTTPException 是一個普通的 exception,包含和 API 相關的附加數據
- 因為是一個 Python exception ,應該 raise 它,而不是 return 它
查看一下 HTTPException 源碼
- status_code:響應狀態嗎
- detail:報錯信息
- headers:響應頭
簡單的栗子
當 item_id 不存在的時候,則拋出 404 錯誤碼
#!usr/bin/env python # -*- coding:utf-8 _*- """ # author: 小菠蘿測試筆記 # blog: https://www.cnblogs.com/poloyy/ # time: 2021/9/22 9:52 上午 # file: 21_File.py """ import uvicorn from fastapi import FastAPI, HTTPException, status 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=status.HTTP_404_NOT_FOUND, detail="item_id 不存在") return {"item": items[item_id]} if __name__ == "__main__": uvicorn.run(app="23_handle_error:app", host="127.0.0.1", port=8080, reload=True, debug=True)
重點
- 可以傳遞任何可以轉換為 JSON 字符串的值給 detail 參數,而不僅僅是 str,可以是 dict、list
- 它們由 FastAPI 自動處理並轉換為 JSON
item_id = foo 的請求結果
找不到 item_id 的請求結果
添加自定義 Headers
在某些情況下,向 HTTP 錯誤添加自定義 Headers 會挺有用的
@app.get("/items-header/{item_id}") async def read_item_header(item_id: str): if item_id not in items: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Item not found", headers={"X-Error": "There goes my error"}, ) return {"item": items[item_id]}
找不到 item_id 的請求結果
自定義 Exception Handlers
背景
- 假設有一個自定義異常 UnicornException
- 希望使用 FastAPI 全局處理此異常
- 可以使用 @app.exception_handler() 添加自定義異常處理程序
實際代碼
#!usr/bin/env python # -*- coding:utf-8 _*- """ # author: 小菠蘿測試筆記 # blog: https://www.cnblogs.com/poloyy/ # time: 2021/9/22 9:52 上午 # file: 21_File.py """ import uvicorn from fastapi import FastAPI, HTTPException, status, Request from fastapi.responses import JSONResponse app = FastAPI() class UnicornException(Exception): def __init__(self, name: str): self.name = name @app.exception_handler(UnicornException) async def unicorn_exception_handler(request: Request, exc: UnicornException): return JSONResponse( status_code=status.HTTP_418_IM_A_TEAPOT, content={"message": f"Oops! {exc.name} did something. "}, ) @app.get("/unicorns/{name}") async def read_unicorn(name: str): if name == "yolo": raise UnicornException(name=name) return {"unicorn_name": name} if __name__ == "__main__": uvicorn.run(app="23_handle_error:app", host="127.0.0.1", port=8080, reload=True, debug=True)
- 如果請求 /unicorns/yolo,將會拋出 UnicornException,但它將由 unicorn_exception_handler 處理
- JSONResponse 將會在后面的文章中詳解
/unicorns/yolo 的請求結果
重寫默認異常處理程序
- FastAPI 有一些默認的異常處理程序
- 比如:當引發 HTTPException 並且請求包含無效數據時,異常處理程序負責返回默認的 JSON 響應
- 可以使用自己的異常處理程序覆蓋(重寫)這些默認的異常處理程序
重寫 HTTPException 異常處理程序
# 導入對應的異常類 from fastapi.exceptions import HTTPException from fastapi.responses import PlainTextResponse # 重寫 HTTPException 異常處理程序 @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): # 原來是返回 JSONResponse,現在改成返回 PlainTextResponse return PlainTextResponse(content=exc.detail, status_code=exc.status_code) @app.get("/items2/{item_id}") async def read_item(item_id: int): if item_id == 3: # 拋出 HTTPException raise HTTPException(status_code=418, detail="Nope! I don't like 3.") return {"item_id": item_id}
item_id = 3 的請求結果
重寫請求驗證異常的處理程序
當請求包含無效數據時,FastAPI 會在內部引發 RequestValidationError,它還包括一個默認的異常處理程序
實際代碼
# 需要先導入對應的異常類 from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse # 重寫 RequestValidationError 異常處理程序 @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): # 返回自定義響應 return PlainTextResponse(str(exc), status_code=status.HTTP_400_BAD_REQUEST) @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}
怎么才會請求驗證失敗?
item_id 聲明為 int,傳一個無法轉成 int 的字符串就會拋出 RequestValidationError,比如 "str"
在沒有重寫 RequestValidationError 異常處理程序前,請求驗證失敗的返回值
{ "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 的 body 屬性
RequestValidationError 包含它收到的帶有無效數據的正文,可以在開發應用程序時使用它來記錄主體並調試它,將其返回給用戶
數據驗證失敗的請求結果
看一眼 RequestValidationError 的源碼
有一個 body 實例屬性
RequestValidationError vs ValidationError
- RequestValidationError 是 Pydantic 的 ValidationError 的子類
- 當使用了 response_model,如果響應數據校驗失敗,就會拋出 ValidationError
- 客戶端並不會直接收到 ValidationError,而是會收到 500,並報 Internal Server Error 服務器錯誤;這意味着就是服務端代碼有問題
- 正常來說,客戶端看不到 ValidationError 是正確的,因為這可能會暴露安全漏洞
報錯后,控制台輸出
raise ValidationError(errors, field.type_) pydantic.error_wrappers.ValidationError: 1 validation error for Item response -> price value is not a valid float (type=type_error.float)
FastAPI 的 HTTPException vs Starlette 的 HTTPException
- FastAPI 的 HTTPException 是 Starlette 的 HTTPException 的子類
- 唯一不同:FastAPI 的 HTTPException 支持自定義 Response Headers,在 OAuth2.0 中這是需要用到的
- 但需要注冊(重寫/重用)一個異常處理程序時,應該用 Starlette 的 HTTPException 來注冊它
- 這樣做的好處:當 Starlette 內部代碼或擴展插件的任何部分引發 HTTPException,自己注冊的異常處理程序都能捕獲並處理它
重用 FastAPI HTTPException 的異常處理程序
重用、重寫的區別
- 重寫:有點像覆蓋的意思,把默認的功能完全改寫
- 重用:仍然會復用默認的功能,但會額外添加一些功能
實際代碼
# 重用 HTTPException from fastapi import FastAPI, HTTPException # 為了重用,需要引入默認的 HTTPException、RequestValidationError 異常處理函數 from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, ) from fastapi.exceptions import RequestValidationError # 避免重名,所以 starlette 的 HTTPException 取一個別名 from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() # HTTPException 異常處理 @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) # RequestVlidationErrpr 異常處理 @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}
引發對應的異常后,控制台會輸出
OMG! An HTTP error!: HTTPException(status_code=418, detail="Nope! I don't like 3.") INFO: 127.0.0.1:57101 - "GET /items/3 HTTP/1.1" 418 I'm a Teapot OMG! The client sent invalid data!: 1 validation error for Request path -> item_id value is not a valid integer (type=type_error.integer) INFO: 127.0.0.1:57119 - "GET /items/s HTTP/1.1" 422 Unprocessable Entity