异常
异常处理¶
SRF 定义了两个异常类,并在 BaseViewSet.as_view() 内捕获部分 ORM、
Pydantic 和 HTTP 异常。它没有安装全局统一异常处理器。
SRF 异常¶
TargetObjectAlreadyExist:HTTPException子类,状态码 409。ImproperlyConfigured:HTTPException子类,状态码 500。
它们只定义状态码和说明,没有自定义 JSON 响应结构。最终响应格式取决于异常 发生位置:在 ViewSet handler 内会经过 SRF 的捕获逻辑,其他路由则由 Sanic 处理。
ViewSet 内置处理¶
BaseViewSet.as_view() 在调用 handler 时处理:
| 异常 | 响应 |
|---|---|
tortoise.exceptions.DoesNotExist |
404 文本响应 |
pydantic.ValidationError |
422 JSON:{"detail": str(error)} |
sanic.exceptions.HTTPException |
异常状态码,JSON:{"detail": ...} |
例如:
from sanic.exceptions import BadRequest
class ProductViewSet(BaseViewSet):
async def create(self, request):
if request.json is None:
raise BadRequest("请求体不能为空")
# ...
权限检查发生在这个 try 块之前,因此 check_permissions() 抛出的
Forbidden 由 Sanic 处理,不经过上述 JSON 转换。
CRUD 的特殊行为¶
get_object()使用get_or_none(),对象不存在时抛出 SanicNotFound。perform_create()把 TortoiseIntegrityError转成 409HTTPException(detail="data conflict")。- 请求 JSON 为
None时,内置 create/update 直接返回空 400 响应。
全局统一响应¶
如果应用需要一致格式,应注册 Sanic 异常处理器:
from sanic.exceptions import HTTPException
from sanic.response import json
@app.exception(HTTPException)
async def handle_http_exception(request, exception):
return json(
{
"error": type(exception).__name__,
"message": getattr(exception, "message", str(exception)),
},
status=exception.status_code,
)
注意:ViewSet 已捕获并转换的异常不会再到达这个全局处理器。若要让所有端点
完全统一,需要同时调整 BaseViewSet.as_view() 的捕获策略。
生产环境可另加兜底处理器,但不要向客户端暴露堆栈:
from sanic.log import error_logger
@app.exception(Exception)
async def handle_unexpected_error(request, exception):
error_logger.exception("Unhandled exception")
return json(
{"error": "INTERNAL_ERROR", "message": "服务器内部错误"},
status=500,
)
Pydantic 错误¶
当前内置响应使用 str(error)。若客户端需要结构化字段错误,可在自定义
ViewSet handler 中返回 error.errors(),或修改框架捕获逻辑:
except ValidationError as error:
return JSONResponse(
{"errors": error.errors(include_url=False)},
status=422,
)
最佳实践¶
- 分类异常:为不同类型的错误使用不同的异常类
- 友好的错误信息:提供清晰、有帮助的错误消息
- 统一响应格式:使用一致的错误响应结构
- 记录详细日志:记录错误的上下文信息
- 隐藏内部细节:生产环境不暴露内部错误
- 使用适当的状态码:为不同错误使用正确的 HTTP 状态码
- 国际化:支持多语言错误消息