跳到内容

处理错误

在很多情况下,你需要向使用 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

你可以传递 dictlist 等。

它们会被 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.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

你可以在开发应用程序时使用它来记录 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

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

{
  "title": "towel",
  "size": "XL"
}

你将收到一个响应,告知你数据无效,其中包含了接收到的 body。

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

在此示例中,你只是用一条表达性很强的消息打印了错误,但你明白了其中的逻辑。你可以使用该异常,然后复用默认的异常处理器。