跳到内容

错误处理

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

此客户端可以是带有前端的浏览器、其他人编写的代码、物联网设备等。

您可能需要告诉客户端,例如:

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

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

这类似于 200 HTTP 状态码(从 200 到 299)。这些“200”状态码意味着请求某种程度上“成功”了。

400 范围内的状态码表示客户端发生错误。

还记得所有那些 **“404 未找到”** 错误(和笑话)吗?

使用 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"),该客户端将收到 HTTP 状态码 200,以及以下 JSON 响应:

{
  "item": "The Foo Wrestlers"
}

但是,如果客户端请求 http://example.com/items/bar(不存在的 item_id"bar"),该客户端将收到 HTTP 状态码 404(“未找到”错误),以及以下 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** 提供了与 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

警告

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

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 提供与 starlette.responses 相同的 fastapi.responses,只是为了方便开发者。但大多数可用的响应都直接来自 Starlette。

使用 RequestValidationError 的响应体

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",
        "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}

在此示例中,您只是用一个非常富有表现力的消息打印错误,但您应该明白这个想法。您可以使用该异常,然后复用默认的异常处理器。