"""统一错误响应构建器。 定义 ``UnifiedError`` 协议(三模块异常基类都实现此协议), 提供 ``build_error_response()`` 和 ``build_error_response_from_exc()`` 统一构建错误响应,确保所有全局异常处理器输出格式一致。 """ from __future__ import annotations from typing import Any, Protocol from fastapi.responses import JSONResponse from yuxi.utils.trace_context import get_trace_id class UnifiedError(Protocol): """统一异常协议。 三模块异常基类(ChannelError / ExternalSystemError / SchedulerError) 都实现此协议,允许统一 handler 处理。 字段: status_code: HTTP 状态码。 error_code: 稳定错误码(字符串,如 "VALIDATION_ERROR")。 message: 人类可读错误信息。 details: 业务字段字典(如 references, retry_after 等)。 trace_id: 链路追踪 ID。 """ status_code: int error_code: str message: str details: dict[str, Any] trace_id: str | None def _extract_retry_after(exc: UnifiedError) -> dict[str, str]: """统一提取 Retry-After 响应头。 429/503 且 details 含 retry_after 时设置。 503 且无 retry_after 时设置默认 60 秒(ChannelDegradedError 等降级场景)。 channels 的 RateLimitError 已将 retry_after 写入 details(通过 to_dict())。 external_systems 的 RateLimitExceededError 已将 retry_after 写入 details。 Args: exc: 满足 UnifiedError 协议的异常对象。 Returns: 含 Retry-After 头的字典(无需要时为空字典)。 """ headers: dict[str, str] = {} if exc.status_code in (429, 503): retry_after = exc.details.get("retry_after") if retry_after is not None: headers["Retry-After"] = str(retry_after) elif exc.status_code == 503: # 503 降级场景无明确 retry_after 时设置默认 60 秒 headers["Retry-After"] = "60" return headers def build_error_response( status_code: int, code: str, message: str, details: dict[str, Any] | None = None, trace_id: str | None = None, headers: dict[str, str] | None = None, ) -> JSONResponse: """构建统一错误响应。 所有全局异常处理器调用此函数,确保响应格式一致。 trace_id 优先使用传入值,其次从 ContextVar 获取。 Args: status_code: HTTP 状态码。 code: 稳定错误码。 message: 人类可读错误信息。 details: 业务字段字典。 trace_id: 链路追踪 ID(为 None 时从 ContextVar 获取)。 headers: 额外响应头(如 Retry-After)。 Returns: 统一格式的 JSONResponse。 """ resolved_trace_id = trace_id or get_trace_id() return JSONResponse( status_code=status_code, content={ "success": False, "error": { "code": code, "message": message, "trace_id": resolved_trace_id, "details": details or {}, }, }, headers=headers or {}, ) def build_error_response_from_exc(exc: UnifiedError) -> JSONResponse: """从统一异常对象构建错误响应(统一入口)。 所有满足 UnifiedError 协议的异常都通过此函数构建响应, 确保 handler 逻辑完全一致,不再有 isinstance 特判。 Args: exc: 满足 UnifiedError 协议的异常对象。 Returns: 统一格式的 JSONResponse。 """ headers = _extract_retry_after(exc) return build_error_response( status_code=exc.status_code, code=exc.error_code, message=exc.message, details=exc.details, trace_id=exc.trace_id, headers=headers, )