在某些情况下,我们需要向使用我们API的客户端提示此错误信息。
客户端可能是一个带前端的浏览器、其他用户使用的代码或者一台物联网设备等。
需要向客户端返回错误提示的场景主要有:
遇到这些情况,我们通常需要返回一个 400
范围(400-499)内的 HTTP 状态码(status code)
。
这和 200
HTTP 状态码(200-299)的使用类似。200
状态码意味着在某妆程度上请求中有一个“成功”。
不过 400
范围内的状态码则表示有一个来自客户端的错误。
就像大家所熟知的 404 NotFound
错误,就是因为客户端访问了一个不存在的URL导致的。
要向客户端返回HTTP错误响应,可以使用 HTTPException
。
from fastapi import FastAPI, HTTPException
HTTPException
就是额外包含了与API相关数据的常规Python异常。
因为是Python异常,所以不能return
, 只能raise
。
这也意味着,如果我们正处在路径操作函数中调用的一个工具方法里,并且我们在工具方法中抛出了一个HTTPException
,那么路径操作函数中的剩余代码将不会再被执行,而是立即终止该请求并将 HTTPException
中的HTTP错误返回给客户端。
在介绍依赖项与安全的章节中,您可以了解更多用 raise
异常代替 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]}
如果客户端请求 item_id
为foo
的项目:http://example.com/items/foo
,则客户端会接收到 200
HTTP状态码,以及JSON响应内容:
{
"item": "The Foo Wrestlers"
}
但是,如果客户端请求一个不存在的item_id
:bar
,http://example.com/items/bar
。那么客户端将收到一个404
HTTP状态码(not found错误)和如下JSON相应内容:
{
"detail": "Item not found"
}
提示:
触发
HTTPException
时,可以用参数detail
传递任何能转换为 JSON 的值,不仅限于str
。还支持传递
dict
、list
等数据结构。FastAPI 能自动处理这些数据,并将之转换为 JSON。
在某些情况下,为HTTP错误添加自定义headers是有用的,例如,出于安全等方面的考虑。
一般情况下可能不需要在代码中直接使用响应headers,但是对于一些高级的场景,如果需要还是可以自己添加自定义headers:
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"}, # 添加自定义headers
)
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}
当我们请求/unicorn/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
也可以用于导入Request
和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):
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
vs ValidationError
:警告:这些是技术细节,如果现在对你来说不重要,可以跳过。
RequestValidationError
是一个Pydantic中ValidationError
的子类。
FastAPI使用它是因为,如果在response_model
中使用了Pydantic模型,同时你的数据中有错误,name你将会在日志中看到错误。
但是客户端/用户不会看到它。相反,客户端会收到一个HTTP状态码为500
的Internal Server Error
错误。
确实应该这样,因为如果在响应或代码中的任何地方(而不是客户端请求中)存在Pydantic ValidationError
,那么它实际上是一个我们代码中的bug。
当我们修复它时,客户端/用户不应该访问有关错误的内部信息,因为这可能会暴露安全漏洞。
同样地,我们也可以覆盖HTTPException
处理器。
例如,我们想要为错误返回一个纯文本响应而不是JSON:
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse # 导入纯文本响应类
from starlette.exceptions import HTTPException as StarletteHTTPException # 导入HTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException) # 定义HTTPException的异常处理器
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.") # 抛出一个HTTPException
return {"item_id": item_id}
技术细节:
还可以使用
from starlette.responses import PlainTextResponse
。FastAPI 提供了与
starlette.responses
相同的fastapi.responses
作为快捷方式,但大部分响应都可以直接从 Starlette 导入。
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}), # 使用exc.body数据
)
class Item(BaseModel):
title: str
size: int
@app.post("/items/")
async def create_item(item: Item):
return item
现在,尝试发送一个无效的项目请求,例如:其中size
的数据类型错误
{
"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
, 并且FastAPI的HTTPException
错误类是继承自Starlette的HTTPException
错误类的。
唯一的区别就是,FastAPI的HTTPException
允许我们在响应中添加自定义headers。
在OAuth 2.0 或其他安全工具内部会需要/使用这些headers。
所以,我们可以在代码中继续像平常一样抛出FastPAI的HTTPException
。
⭐ 但是,当我们要注册异常处理器时,则应该使用Starlette的HTTPException
来注册。
这样,如果 Starlette 的内部代码、 Starlette 扩展或插件的任何部分引发了 Starlette HTTPException
异常,我们的处理程序将能够捕获并处理它。
在本例中,为了能够在代码中同时包含两种 HTTPException
,Starlette 的异常被重命名为 StarletteHTTPException
:
from starlette.exceptions import HTTPException as StarletteHTTPException
如果想要将异常和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}
在本例中,我们只是用一条非常夸张的消息打印错误,但我们知道,我们可以先使用异常,然后重新使用默认的异常处理器。