"""全局异常处理器集中实现。 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``, 本处理器统一捕获并转换为项目格式。状态码保持 422(FastAPI 标准), 仅统一响应结构。 """ 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}, )