跳至内容

处理错误

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

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

您可能需要告诉客户端

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

在这些情况下,您通常会在 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 错误发送到客户端。

在关于依赖项和安全性的部分中,引发异常而不是return值的优势将更加明显。

在此示例中,当客户端请求不存在 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提供与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提供与fastapi.responses相同的starlette.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

并且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}

在此示例中,您只是用非常有表现力的消息print错误,但您明白了。您可以使用异常,然后只需重用默认的异常处理程序。