处理错误¶
🌐 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.
某些情况下,需要向使用你的 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/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"
}
提示
触发 HTTPException 时,可以用参数 detail 传递任何能转换为 JSON 的值,不仅限于 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]}
🤓 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 Request 和 from 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。
FastAPI 的 HTTPException 错误类继承自 Starlette 的 HTTPException 错误类。
它们之间的唯一区别是,FastAPI 的 HTTPException 在 detail 字段中接受任意可转换为 JSON 的数据,而 Starlette 的 HTTPException 只接受字符串。
因此,你可以继续像平常一样在代码中触发 FastAPI 的 HTTPException。
但注册异常处理器时,应该注册到来自 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}
虽然本例只是用非常夸张的信息打印了错误,但足以说明:你可以先处理异常,然后再复用默认的异常处理器。