Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

304 lines
10 KiB
Python

"""
统一错误处理中间件 V2
使用新的错误响应格式,提供更好的错误处理和用户反馈
"""
import logging
import traceback
import time
import asyncio
import functools
from typing import Union
from contextlib import contextmanager
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from ..utils.error_handler import AutoClipsException, ErrorCategory, ErrorLevel
from ..services.exceptions import ServiceError
from ..utils.error_response import (
create_error_response,
create_validation_error_response,
create_exception_error_response,
create_http_exception_response,
ErrorCode,
ErrorLevel as ResponseErrorLevel
)
logger = logging.getLogger(__name__)
def get_request_id(request: Request) -> str:
"""获取请求ID"""
return getattr(request.state, 'request_id', None) or str(uuid.uuid4())
def log_error(error: Exception, request: Request, context: str = None):
"""记录错误日志"""
request_id = get_request_id(request)
error_info = {
"request_id": request_id,
"path": request.url.path,
"method": request.method,
"error_type": type(error).__name__,
"error_message": str(error),
"context": context,
"traceback": traceback.format_exc()
}
logger.error(f"Error in {context or 'unknown context'}: {error}", extra=error_info)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""全局异常处理器"""
request_id = get_request_id(request)
# 记录异常详情
log_error(exc, request, "GlobalExceptionHandler")
# 根据异常类型返回不同的错误响应
if isinstance(exc, AutoClipsException):
return handle_autoclips_exception(exc, request_id)
elif isinstance(exc, ServiceError):
return handle_service_error(exc, request_id)
elif isinstance(exc, HTTPException):
return create_http_exception_response(exc, request_id)
elif isinstance(exc, RequestValidationError):
return create_validation_error_response(exc.errors(), request_id)
elif isinstance(exc, StarletteHTTPException):
return create_http_exception_response(HTTPException(exc.status_code, exc.detail), request_id)
else:
return create_exception_error_response(exc, request_id, "GlobalExceptionHandler")
def handle_autoclips_exception(exc: AutoClipsException, request_id: str = None) -> JSONResponse:
"""处理AutoClipsException"""
# 根据错误分类映射到新的错误代码
error_code_mapping = {
ErrorCategory.CONFIGURATION: ErrorCode.INVALID_PARAMETER,
ErrorCategory.NETWORK: ErrorCode.NETWORK_ERROR,
ErrorCategory.API: ErrorCode.EXTERNAL_API_ERROR,
ErrorCategory.FILE_IO: ErrorCode.FILE_PROCESSING_ERROR,
ErrorCategory.PROCESSING: ErrorCode.PROCESSING_ERROR,
ErrorCategory.VALIDATION: ErrorCode.VALIDATION_ERROR,
ErrorCategory.SYSTEM: ErrorCode.INTERNAL_SERVER_ERROR,
}
error_code = error_code_mapping.get(exc.category, ErrorCode.UNKNOWN_ERROR)
# 根据错误级别映射到响应错误级别
level_mapping = {
ErrorLevel.DEBUG: ResponseErrorLevel.INFO,
ErrorLevel.INFO: ResponseErrorLevel.INFO,
ErrorLevel.WARNING: ResponseErrorLevel.WARNING,
ErrorLevel.ERROR: ResponseErrorLevel.ERROR,
ErrorLevel.CRITICAL: ResponseErrorLevel.CRITICAL,
}
level = level_mapping.get(exc.level, ResponseErrorLevel.ERROR)
return create_error_response(
error_code=error_code,
message=exc.message,
user_message=exc.user_message,
details=exc.details,
level=level,
request_id=request_id
)
def handle_service_error(exc: ServiceError, request_id: str = None) -> JSONResponse:
"""处理ServiceError"""
# 根据服务错误代码映射到新的错误代码
error_code_mapping = {
"CONFIGURATION_ERROR": ErrorCode.INVALID_PARAMETER,
"NETWORK_ERROR": ErrorCode.NETWORK_ERROR,
"API_ERROR": ErrorCode.EXTERNAL_API_ERROR,
"FILE_IO_ERROR": ErrorCode.FILE_PROCESSING_ERROR,
"PROCESSING_ERROR": ErrorCode.PROCESSING_ERROR,
"VALIDATION_ERROR": ErrorCode.VALIDATION_ERROR,
"SYSTEM_ERROR": ErrorCode.INTERNAL_SERVER_ERROR,
}
error_code = error_code_mapping.get(exc.error_code.value, ErrorCode.UNKNOWN_ERROR)
return create_error_response(
error_code=error_code,
message=exc.message,
details=exc.details,
request_id=request_id
)
# 装饰器:自动错误处理
def handle_errors(error_category: ErrorCategory = ErrorCategory.SYSTEM):
"""错误处理装饰器"""
def decorator(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except AutoClipsException:
# 重新抛出AutoClipsException
raise
except ServiceError:
# 重新抛出ServiceError
raise
except Exception as e:
# 转换为AutoClipsException
raise AutoClipsException(
message=str(e),
category=error_category,
original_exception=e
)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except AutoClipsException:
# 重新抛出AutoClipsException
raise
except ServiceError:
# 重新抛出ServiceError
raise
except Exception as e:
# 转换为AutoClipsException
raise AutoClipsException(
message=str(e),
category=error_category,
original_exception=e
)
# 根据函数类型返回对应的包装器
if asyncio.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
return decorator
# 上下文管理器:错误上下文
@contextmanager
def error_context(category: ErrorCategory, context_info: dict = None):
"""错误上下文管理器"""
try:
yield
except Exception as e:
if isinstance(e, AutoClipsException):
# 已经是自定义异常,直接抛出
raise
else:
# 转换为自定义异常
details = context_info or {}
details["original_exception_type"] = type(e).__name__
raise AutoClipsException(
message=str(e),
category=category,
details=details,
original_exception=e
)
# 错误统计和监控
class ErrorMonitor:
"""错误监控器"""
def __init__(self):
self.error_counts = {}
self.error_history = []
self.max_history_size = 1000
def record_error(self, error: Exception, context: str = None):
"""记录错误"""
error_type = type(error).__name__
key = f"{error_type}:{context or 'unknown'}"
# 更新错误计数
self.error_counts[key] = self.error_counts.get(key, 0) + 1
# 记录错误历史
error_record = {
"timestamp": time.time(),
"error_type": error_type,
"error_message": str(error),
"context": context,
"traceback": traceback.format_exc()
}
self.error_history.append(error_record)
# 限制历史记录大小
if len(self.error_history) > self.max_history_size:
self.error_history = self.error_history[-self.max_history_size:]
def get_error_stats(self) -> dict:
"""获取错误统计"""
return {
"error_counts": self.error_counts,
"total_errors": sum(self.error_counts.values()),
"recent_errors": self.error_history[-10:] if self.error_history else []
}
def clear_stats(self):
"""清除统计信息"""
self.error_counts.clear()
self.error_history.clear()
# 全局错误监控器实例
error_monitor = ErrorMonitor()
# 错误恢复机制
class ErrorRecovery:
"""错误恢复机制"""
def __init__(self):
self.recovery_strategies = {}
def register_strategy(self, error_type: type, strategy_func):
"""注册恢复策略"""
self.recovery_strategies[error_type] = strategy_func
def attempt_recovery(self, error: Exception, context: str = None) -> bool:
"""尝试错误恢复"""
error_type = type(error)
if error_type in self.recovery_strategies:
try:
return self.recovery_strategies[error_type](error, context)
except Exception as recovery_error:
logger.error(f"Recovery strategy failed: {recovery_error}")
return False
return False
# 全局错误恢复器实例
error_recovery = ErrorRecovery()
# 注册一些基本的恢复策略
def network_error_recovery(error: Exception, context: str = None) -> bool:
"""网络错误恢复策略"""
# 这里可以实现网络重连、切换备用服务器等逻辑
logger.info(f"Attempting network error recovery for: {context}")
return False
def file_error_recovery(error: Exception, context: str = None) -> bool:
"""文件错误恢复策略"""
# 这里可以实现文件重试、使用备用文件等逻辑
logger.info(f"Attempting file error recovery for: {context}")
return False
# 注册恢复策略
error_recovery.register_strategy(ConnectionError, network_error_recovery)
error_recovery.register_strategy(FileNotFoundError, file_error_recovery)
error_recovery.register_strategy(PermissionError, file_error_recovery)