处理错误¶
在许多情况下,您需要向使用您 API 的客户端通知错误。
这个客户端可能是一个带有前端的浏览器、其他人的代码、一个物联网设备等。
您可能需要告诉客户端:
- 客户端没有足够的权限执行该操作。
- 客户端无法访问该资源。
- 客户端尝试访问的条目不存在。
- 等等。
在这些情况下,您通常会返回一个在 400 范围内的 HTTP 状态码(从 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/foo(一个 item_id 为 "foo"),该客户端将收到一个 HTTP 状态码 200,以及一个 JSON 响应:
{
"item": "The Foo Wrestlers"
}
但如果客户端请求 http://example.com/items/bar(一个不存在的 item_id "bar"),该客户端将收到一个 HTTP 状态码 404(“not found”错误),以及一个 JSON 响应:
{
"detail": "Item not found"
}
Tip
当抛出 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 Request 和 from 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)
RequestValidationError 与 ValidationError¶
Warning
这些是技术细节,如果现在对您不重要,可以跳过。
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 包含它接收到的带有无效数据的请求体。
您可以在开发应用程序时使用它来记录请求体并进行调试,将其返回给用户等。
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
现在尝试发送一个无效的条目,如:
{
"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}
在这个例子中,您只是用一个非常表达性的消息打印错误,但您明白了思路。您可以使用异常,然后只需重用默认的异常处理器。