跳转至

处理错误

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

这个客户端可能是一个带有前端的浏览器、其他人的代码、一个物联网设备等。

您可能需要告诉客户端:

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

在这些情况下,您通常会返回一个在 400 范围内的 HTTP 状态码(从 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/foo(一个 item_id"foo"),该客户端将收到一个 HTTP 状态码 200,以及一个 JSON 响应:

{
  "item": "The Foo Wrestlers"
}

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

{
  "detail": "Item not found"
}

Tip

当抛出 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 提供与 fastapi.responses 相同的 starlette.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):
    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,您将不会得到默认的 JSON 错误:

{
    "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)

RequestValidationErrorValidationError

Warning

这些是技术细节,如果现在对您不重要,可以跳过。

RequestValidationError 是 Pydantic 的 ValidationError 的子类。

FastAPI 使用它,以便当您在 response_model 中使用 Pydantic 模型,并且您的数据有错误时,您将在日志中看到错误。

但客户端/用户不会看到它。相反,客户端将收到一个“内部服务器错误”,HTTP 状态码为 500

应该是这样的,因为如果您在响应中或代码中的任何地方(而不是客户端的请求中)有 Pydantic ValidationError,这实际上是您代码中的一个错误。

在您修复它时,您的客户端/用户不应该能够访问有关错误的内部信息,因为这可能暴露安全漏洞。

覆盖 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):
    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}

技术细节

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

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

使用 RequestValidationError 的请求体

RequestValidationError 包含它接收到的带有无效数据的请求体。

您可以在开发应用程序时使用它来记录请求体并进行调试,将其返回给用户等。

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

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

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

因此,您可以像往常一样在代码中继续抛出 FastAPIHTTPException

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

在这个例子中,您只是用一个非常表达性的消息打印错误,但您明白了思路。您可以使用异常,然后只需重用默认的异常处理器。