test/app/core/exceptions.py

225 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
"""
统一异常处理模块
定义所有自定义异常类和错误处理函数
"""
from datetime import datetime, timezone
from fastapi import HTTPException, Request
from fastapi.exception_handlers import (
http_exception_handler as fastapi_http_exception_handler,
request_validation_exception_handler as fastapi_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
import logging
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
def _uses_meeting_legacy_response(request: Request) -> bool:
"""Return True for Model-Test-New compatible offline meeting/speaker APIs."""
path = request.url.path.rstrip("/")
method = request.method.upper()
if path.endswith("/asr/transcriptions"):
return method == "POST"
if "/asr/transcriptions/" in path:
return method == "GET"
if path.endswith("/speakers"):
return method == "POST"
return False
def get_iso_timestamp() -> str:
"""获取ISO 8601格式的UTC时间戳"""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def create_error_response(
error_code: str,
message: str,
task_id: str = "",
details: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
创建标准错误响应格式
Args:
error_code: 错误代码(如 INVALID_PARAMETER)
message: 人类可读的错误信息
task_id: 任务ID(可选)
details: 额外详细信息(可选)
Returns:
标准错误响应字典
"""
return {
"error_code": error_code,
"message": message,
"task_id": task_id or "",
"timestamp": get_iso_timestamp(),
"details": details or {},
}
def get_http_status_code(status_code: int) -> int:
"""Map internal API status code to HTTP status code."""
return 500 if status_code >= 50000000 else 400
class APIException(Exception):
"""API基础异常类"""
def __init__(
self,
status_code: int,
message: str,
task_id: str = "",
error_code: str = "",
details: Optional[Dict[str, Any]] = None,
):
self.status_code = status_code
self.message = message
self.task_id = task_id
self.error_code = error_code or self._get_error_code(status_code)
self.details = details or {}
super().__init__(self.message)
def _get_error_code(self, status_code: int) -> str:
"""根据状态码获取错误代码"""
code_mapping = {
20000000: "SUCCESS",
40000000: "DEFAULT_CLIENT_ERROR",
40000001: "AUTHENTICATION_FAILED",
40000002: "INVALID_MESSAGE",
40000003: "INVALID_PARAMETER",
40000004: "IDLE_TIMEOUT",
40000005: "TOO_MANY_REQUESTS",
40000010: "TRIAL_EXPIRED",
41010101: "UNSUPPORTED_SAMPLE_RATE",
50000000: "DEFAULT_SERVER_ERROR",
50000001: "INTERNAL_GRPC_ERROR",
}
return code_mapping.get(status_code, "UNKNOWN_ERROR")
def to_dict(self) -> Dict[str, Any]:
"""
将异常转换为标准错误响应字典
Returns:
标准错误响应字典,包含 error_code, message, task_id, timestamp, details
"""
return create_error_response(
error_code=self.error_code,
message=self.message,
task_id=self.task_id,
details=self.details,
)
# 标准异常类
class AuthenticationException(APIException):
"""身份认证异常"""
def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None):
super().__init__(40000001, message, task_id, details=details)
class InvalidMessageException(APIException):
"""无效消息异常"""
def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None):
super().__init__(40000002, message, task_id, details=details)
class InvalidParameterException(APIException):
"""无效参数异常"""
def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None):
super().__init__(40000003, message, task_id, details=details)
class UnsupportedSampleRateException(APIException):
"""不支持的采样率异常"""
def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None):
super().__init__(41010101, message, task_id, details=details)
class DefaultServerErrorException(APIException):
"""默认服务端错误异常"""
def __init__(self, message: str, task_id: str = "", details: Optional[Dict[str, Any]] = None):
super().__init__(50000000, message, task_id, details=details)
# 异常处理器
async def api_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""API异常处理器"""
# FastAPI 只会在抛出 APIException 时调用此处理器
api_exc = exc if isinstance(exc, APIException) else APIException(50000000, str(exc))
logger.error(f"[{api_exc.task_id}] API异常: {api_exc.message}")
# 使用标准错误格式
response_data = api_exc.to_dict()
# 确定HTTP状态码
http_status_code = get_http_status_code(api_exc.status_code)
return JSONResponse(
content=response_data,
headers={"task_id": api_exc.task_id} if api_exc.task_id else {},
status_code=http_status_code,
)
async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""FastAPI HTTPException handler for legacy meeting/speaker endpoints."""
http_exc = exc if isinstance(exc, HTTPException) else HTTPException(500, str(exc))
if not _uses_meeting_legacy_response(request):
return await fastapi_http_exception_handler(request, http_exc)
detail = http_exc.detail
if isinstance(detail, dict):
return JSONResponse(
status_code=http_exc.status_code,
content={
"code": detail.get("code", 4001),
"message": detail.get("message", "Request failed"),
},
)
return JSONResponse(
status_code=http_exc.status_code,
content={"code": 4001, "message": str(detail)},
)
async def validation_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""FastAPI/Pydantic validation handler for legacy meeting/speaker endpoints."""
validation_exc = exc if isinstance(exc, RequestValidationError) else None
if validation_exc is None:
return JSONResponse(
status_code=422,
content={"code": 1001, "message": "Invalid request"},
)
if not _uses_meeting_legacy_response(request):
return await fastapi_validation_exception_handler(request, validation_exc)
errors = validation_exc.errors()
message = str(errors[0]["msg"]) if errors else "Invalid request"
return JSONResponse(status_code=422, content={"code": 1001, "message": message})
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""通用异常处理器"""
logger.error(f"未处理的异常: {str(exc)}", exc_info=True)
# 使用标准错误格式
response_data = create_error_response(
error_code="DEFAULT_SERVER_ERROR",
message=f"内部服务错误: {str(exc)}",
)
return JSONResponse(content=response_data, status_code=500)