首页 > 技术文章 > FastAPI 进阶知识(三) 错误处理

mazhiyong 2020-05-28 16:53 原文

作者:麦克煎蛋   出处:https://www.cnblogs.com/mazhiyong/ 转载请保留这段声明,谢谢!

 

如果使用API时有错误发生,你需要通知给客户端(Web端或者API使用者)这个错误信息。

常见的错误信息为:

  • 客户端没有权限进行相关的操作。
  • 客户端找不到对应的路径操作。
  • 客户端找不到对应的资源。
  • 其他。

这些错误信息的HTTP状态码一般为400错误(400~499)。

 

一、HTTPException

我们用HTTPException模块返回带错误信息的Response。

HTTPException是一个普通的Python异常,同时带有与API访问有关的附加数据。

1、导入模块

from fastapi import HTTPException

2、抛出异常

在代码中抛出HTTPException

raise HTTPException(status_code=404, detail="Item not found")

这里, 参数detail除了可以传递字符串,还可以传递任何可以转换成JSON格式的数据,如dict、list等。

 

代码示例:

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错误,在一些场景下,我们需要添加自定义头信息。

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]}

三、自定义异常处理器

借助 the same exception utilities from Starlette,我们可以添加自定义异常处理器。

假设我们有个自定义异常 UnicornException,我们想在全局范围内处理这个异常。

借助 @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,路径操作函数就会抛出异常 UnicornException,这个异常会被我们的异常处理器unicorn_exception_handler捕获到。

最后我们收到的HTTP错误码就是418,并且错误内容为:

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

四、重写缺省异常处理器

FastAPI有一些缺省的异常处理器。当我们抛出HTTPException异常或者当请求有非法数据的时候,这些处理器负责返回默认的JSON结果。

我们可以重写这些异常处理器。

1、重写请求校验异常处理器

当一个请求包含非法数据的时候,FastAPI内部会抛出RequestValidationError异常,并且有默认的异常处理器来处理。

我们可以用 @app.exception_handler(RequestValidationError) 来重写这个异常处理器。

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse

app = FastAPI()

@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,那么返回结果就不是默认的:

{
    "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字段,包含了请求内容的原文。

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",
        "item",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

2、重写HTTPException异常处理器

同样的方法,我们可以重写HTTPException异常处理器。

例如,你可能想返回纯文本格式而不是JSON格式的错误信息。

from fastapi import FastAPI, HTTPException
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.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/3,这时候返回的错误信息为:

Nope! I don't like 3.

如果没有自定义异常处理器http_exception_handler,返回的错误信息为:

{
    "detail": "Nope! I don't like 3."
}

五、重用缺省异常处理器 

我们可以导入并且重用缺省的异常处理器。

我们从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!: {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}

在示例中,我们在抛出异常之前添加了一条日志输出。我们可以根据业务需求灵活的重用缺省异常处理器。

六、FastAPI HTTPException 对比 Starlette HTTPException

FastAPI HTTPException 继承自 Starlette's HTTPException

唯一的区别是,FastAPI HTTPException允许你在response添加头信息。主要在内部用于OAuth 2.0以及一些安全相关的功能。

因此,通常我们在代码中抛出FastAPI HTTPException异常。

 

但是,当我们注册异常处理器的时候,我们应该注册为Starlette HTTPException

这样,当Starlette的内部代码或者Starlette扩展插件抛出Starlette HTTPException时,我们的处理器才能正常捕获和处理这个异常。

 

如果我们要在代码中同时使用这两个类,为了避免命名冲突,我们可以重命名其中一个类。

from fastapi import HTTPException
from starlette.exceptions import HTTPException as StarletteHTTPException

 

推荐阅读