225 lines
7.4 KiB
Python
225 lines
7.4 KiB
Python
# -*- 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)
|