ForcePilot/backend/server/utils/error_response.py

123 lines
3.8 KiB
Python
Raw Normal View History

"""统一错误响应构建器。
定义 ``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,
)