ForcePilot/backend/server/utils/error_handlers.py

93 lines
3.3 KiB
Python
Raw Normal View History

"""全局异常处理器集中实现。
3 handler
- unified_error_handler处理三模块异常满足 UnifiedError 协议
- request_validation_error_handler处理 Pydantic 请求体校验失败
- unhandled_exception_handler兜底原生 Exception
"""
from __future__ import annotations
import logging
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from yuxi.channels.contract.errors import Error as ChannelError
from yuxi.external_systems.exceptions import ExternalSystemError
from yuxi.scheduler.exceptions import SchedulerError
from server.utils.error_response import build_error_response, build_error_response_from_exc
async def unified_error_handler(
_request: Request,
exc: ChannelError | ExternalSystemError | SchedulerError,
) -> JSONResponse:
"""三模块异常统一处理器。
三模块异常基类都满足 UnifiedError 协议status_code/error_code/message/
details/trace_id通过 build_error_response_from_exc() 统一构建响应
不再有 isinstance 特判不再有查表/类属性分支逻辑完全相同
Args:
_request: FastAPI 请求对象未使用
exc: 三模块异常之一
Returns:
统一格式的错误响应
"""
return build_error_response_from_exc(exc)
async def unhandled_exception_handler(_request: Request, exc: Exception) -> JSONResponse:
"""原生异常兜底处理器。
所有未被特定异常处理器捕获的 Exception 在此统一兜底
返回结构化 500 响应trace_id 自动从 ContextVar 获取
Args:
_request: FastAPI 请求对象未使用
exc: 未被捕获的原生异常
Returns:
500 内部服务器错误响应
"""
logging.getLogger("uvicorn.error").exception("unhandled exception", exc_info=exc)
return build_error_response(
status_code=500,
code="INTERNAL",
message="Internal server error",
)
async def request_validation_error_handler(
_request: Request,
exc: RequestValidationError,
) -> JSONResponse:
"""Pydantic 请求体校验失败统一处理器。
FastAPI 默认的 422 + ``{"detail": [...]}`` 转换为项目统一格式
422 + ``{"success": false, "error": {...}}``确保响应格式一致
Pydantic v2 ``model_validator`` / ``field_validator`` 中抛出的
``ValueError`` 子类含项目的 ``ValidationError``会被包装为
``pydantic.ValidationError``再由 FastAPI 转为 ``RequestValidationError``
本处理器统一捕获并转换为项目格式状态码保持 422FastAPI 标准
仅统一响应结构
"""
errors = exc.errors()
first = errors[0] if errors else {}
loc = first.get("loc", [])
field = ".".join(str(part) for part in loc) if loc else ""
message = first.get("msg", "request validation failed")
# Pydantic 对 ValueError 子类的包装会加 "Value error, " 前缀,去除以还原原始 message
if message.startswith("Value error, "):
message = message[len("Value error, ") :]
return build_error_response(
status_code=422,
code="VALIDATION_ERROR",
message=message,
details={"field": field, "errors": errors},
)