跳到内容

处理错误

在许多情况下,你需要向使用你的 API 的客户端通知错误。

该客户端可能是带有前端的浏览器、别人的代码、IoT 设备等。

你可能需要告诉客户端

  • 客户端没有足够的权限执行该操作。
  • 客户端无权访问该资源。
  • 客户端尝试访问的项目不存在。
  • 等等。

在这些情况下,你通常会返回 **HTTP 状态码**,其范围在 **400**(从 400 到 499)之内。

这与 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 错误发送给客户端。

引发异常而不是返回值的好处将在关于依赖项和安全的部分中更加明显。

在此示例中,当客户端按不存在的 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"),该客户端将收到一个 200 的 HTTP 状态码,以及一个 JSON 响应:

{
  "item": "The Foo Wrestlers"
}

但是,如果客户端请求 http://example.com/items/bar(一个不存在的 item_id "bar"),该客户端将收到一个 404 的 HTTP 状态码(“未找到”错误),以及一个 JSON 响应:

{
  "detail": "Item not found"
}

提示

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

安装自定义异常处理程序

你可以使用 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 相同的 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: 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}

技术细节

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

FastAPI 提供与 fastapi.responses 相同的 starlette.responses 只是为了方便开发者。但大多数可用的响应直接来自 Starlette。

警告

请注意,RequestValidationError 包含验证错误发生的文件名和行号信息,以便你可以在日志中显示它,并包含相关信息,如果你愿意的话。

但是,如果你只是将其转换为字符串并直接返回该信息,你可能会泄露有关你的系统的一些信息,这就是为什么这里的代码会分别提取和显示每个错误。

使用 RequestValidationError 的主体

RequestValidationError 包含它接收到的具有无效数据的 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

现在尝试发送一个无效的项目,例如:

{
  "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 与 Starlette 的 HTTPException

FastAPI 有自己的 HTTPException

并且 **FastAPI** 的 HTTPException 错误类继承自 Starlette 的 HTTPException 错误类。

唯一的区别是 **FastAPI** 的 HTTPException 接受任何可 JSON 化数据作为 detail 字段,而 Starlette 的 HTTPException 只接受字符串。

因此,你可以在代码中继续像平常一样引发 **FastAPI** 的 HTTPException

但是,当注册异常处理程序时,你应该为 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}

在这个例子中,你只是打印了一个非常富有表现力的错误消息,但你明白了。你可以使用异常,然后只是重用默认的异常处理程序。