处理错误¶
在许多情况下,你需要向使用你的API的客户端通知错误。
这个客户端可能是一个带有前端的浏览器、其他人的代码、一个物联网设备等。
你可能需要告诉客户端:
- 客户端没有足够的权限执行该操作。
- 客户端无法访问该资源。
- 客户端尝试访问的项目不存在。
- 等等。
在这些情况下,你通常会返回一个**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
是一个带有与API相关的额外数据的普通Python异常。
因为它是一个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(“未找到”错误),以及一个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
,你(或你使用的库)可能会引发它。
并且你希望在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 提供了与 fastapi.responses
相同的 starlette.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):
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
¶
/// 警告
这些是技术细节,如果你现在不关心这些,可以跳过。
///
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
的 body¶
RequestValidationError
包含它接收到的带有无效数据的 body
。
你可以在开发应用时使用它来记录 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
现在尝试发送一个无效的 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
。
并且 FastAPI 的 HTTPException
错误类继承自 Starlette 的 HTTPException
错误类。
唯一的区别是 FastAPI 的 HTTPException
接受 detail
字段的任何可 JSON 化的数据,而 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}
在这个例子中,你只是用一个非常表达性的消息 print
错误,但你明白了。你可以使用异常,然后只是重用默认的异常处理程序。