FastAPI(27)- Handling Errors 處理錯誤


前言

許多情況下,需要向客戶端返回一些特定的錯誤,比如

  • 客戶端沒有足夠的權限進行該操作
  • 客戶端無權訪問該資源
  • 客戶端嘗試訪問的項目不存在

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM