跳转至

处理错误

🌐 由 AI 与人类协作翻译

本翻译由人类引导的 AI 生成。🤝

可能存在误解原意或不够自然等问题。🤖

你可以通过帮助我们更好地引导 AI LLM来改进此翻译。

英文版本

某些情况下,需要向使用你的 API 的客户端返回错误提示。

这里所谓的客户端包括前端浏览器、他人的代码、物联网设备等。

你可能需要告诉客户端:

  • 客户端没有执行该操作的权限
  • 客户端没有访问该资源的权限
  • 客户端要访问的项目不存在
  • 等等

遇到这些情况时,通常要返回 400 范围内(400 至 499)的 HTTP 状态码。

这与 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 是额外包含了和 API 有关数据的常规 Python 异常。

因为是 Python 异常,所以不能 return,只能 raise。

这也意味着,如果你在路径操作函数里调用的某个工具函数内部触发了 HTTPException,那么路径操作函数中后续的代码将不会继续执行,请求会立刻终止,并把 HTTPException 的 HTTP 错误发送给客户端。

在介绍依赖项与安全的章节中,你可以更直观地看到用 raise 异常代替 return 值的优势。

本例中,客户端用不存在的 ID 请求 item 时,触发状态码为 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(“未找到”错误)及如下 JSON 响应结果:

{
  "detail": "Item not found"
}

提示

触发 HTTPException 时,可以用参数 detail 传递任何能转换为 JSON 的值,不仅限于 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 时,路径操作会触发 UnicornException。

但该异常将会被 unicorn_exception_handler 处理。

你会收到清晰的错误信息,HTTP 状态码为 418,JSON 内容如下:

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

技术细节

也可以使用 from starlette.requests import Request 和 from starlette.responses import JSONResponse。

FastAPI 提供了与 starlette.responses 相同的 fastapi.responses 作为便捷方式,但大多数可用的响应都直接来自 Starlette。Request 也是如此。

覆盖默认异常处理器

FastAPI 自带了一些默认异常处理器。

当你触发 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 提供了与 starlette.responses 相同的 fastapi.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

现在试着发送一个无效的 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 vs Starlette 的 HTTPException

FastAPI 也提供了自有的 HTTPException。

FastAPI 的 HTTPException 错误类继承自 Starlette 的 HTTPException 错误类。

它们之间的唯一区别是,FastAPI 的 HTTPException 在 detail 字段中接受任意可转换为 JSON 的数据,而 Starlette 的 HTTPException 只接受字符串。

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

但注册异常处理器时,应该注册到来自 Starlette 的 HTTPException。

这样做是为了,当 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}

虽然本例只是用非常夸张的信息打印了错误,但足以说明:你可以使用该异常,然后直接复用默认的异常处理器。