2026-07-07 16:25:47 +08:00
|
|
|
|
"""全局异常处理器集中实现。
|
|
|
|
|
|
|
|
|
|
|
|
3 个 handler:
|
|
|
|
|
|
- unified_error_handler:处理三模块异常(满足 UnifiedError 协议)
|
|
|
|
|
|
- request_validation_error_handler:处理 Pydantic 请求体校验失败
|
|
|
|
|
|
- unhandled_exception_handler:兜底原生 Exception
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-11 22:06:21 +08:00
|
|
|
|
import json
|
2026-07-07 16:25:47 +08:00
|
|
|
|
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,
|
2026-07-11 22:06:21 +08:00
|
|
|
|
code="INTERNAL_SERVER_ERROR",
|
|
|
|
|
|
message="服务器内部错误,请稍后重试",
|
|
|
|
|
|
details={"hint": "如果问题持续,请联系管理员并提供 trace_id"},
|
2026-07-07 16:25:47 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 标准),
|
|
|
|
|
|
仅统一响应结构。
|
2026-07-11 22:06:21 +08:00
|
|
|
|
|
|
|
|
|
|
``exc.errors()`` 返回的 error dict 中 ``ctx`` 字段可能携带非 JSON 可序列化
|
|
|
|
|
|
对象(如 ``field_validator`` 抛出 ``ValueError`` 时 ctx 为
|
|
|
|
|
|
``{"error": <ValueError 实例>}``)。直接放入 ``JSONResponse`` 会触发
|
|
|
|
|
|
``TypeError``,被 ``unhandled_exception_handler`` 兜底为 500。此处通过
|
|
|
|
|
|
``json.dumps(..., default=str)`` 将非序列化对象降级为字符串,确保响应
|
|
|
|
|
|
始终可序列化。
|
2026-07-07 16:25:47 +08:00
|
|
|
|
"""
|
2026-07-11 22:06:21 +08:00
|
|
|
|
errors = json.loads(json.dumps(exc.errors(), default=str))
|
2026-07-07 16:25:47 +08:00
|
|
|
|
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},
|
|
|
|
|
|
)
|