跳转至

处理错误

🌐 Translation by AI and humans

This translation was made by AI guided by humans. 🤝

It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖

You can improve this translation by helping us guide the AI LLM better.

English version

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

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

你可能需要告诉客户端:

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

遇到这些情况时,通常要返回 4XX(400 至 499)HTTP 状态码

这与表示请求成功的 2XX(200 至 299)HTTP 状态码类似。那些“200”状态码表示某种程度上的“成功”。

4XX 状态码表示客户端发生了错误。

大家都知道「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]}
🤓 Other versions and variants
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]}
🤓 Other versions and variants
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 时,可以用参数 detail 传递任何能转换为 JSON 的值,不仅限于 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]}
🤓 Other versions and variants
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}
🤓 Other versions and variants
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 Requestfrom 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}
🤓 Other versions and variants
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}
🤓 Other versions and variants
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
🤓 Other versions and variants
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

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

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

因此,你可以继续像平常一样在代码中触发 FastAPIHTTPException

但注册异常处理器时,应该注册到来自 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}
🤓 Other versions and variants
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}

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