78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
import uuid
|
|
import logging
|
|
|
|
from rest_framework.views import exception_handler as drf_exception_handler
|
|
from rest_framework.response import Response
|
|
from rest_framework.exceptions import APIException, ValidationError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_STATUS_TO_CODE = {
|
|
400: 'BAD_REQUEST',
|
|
401: 'AUTH_UNAUTHORIZED',
|
|
403: 'PERMISSION_DENIED',
|
|
404: 'NOT_FOUND',
|
|
405: 'METHOD_NOT_ALLOWED',
|
|
429: 'SYS_RATE_LIMIT',
|
|
500: 'SYS_INTERNAL',
|
|
503: 'SYS_DEPENDENCY_DOWN',
|
|
}
|
|
|
|
|
|
def custom_exception_handler(exc, context):
|
|
trace_id = uuid.uuid4().hex[:12]
|
|
response = drf_exception_handler(exc, context)
|
|
|
|
if response is None:
|
|
logger.exception('Unhandled server error trace_id=%s', trace_id)
|
|
return Response(
|
|
{'code': 'SYS_INTERNAL', 'message': '服务器内部错误', 'details': None, 'trace_id': trace_id},
|
|
status=500,
|
|
)
|
|
|
|
data = response.data
|
|
status_code = response.status_code
|
|
|
|
# Already structured by AppError or our custom exceptions
|
|
if isinstance(data, dict) and 'code' in data and 'message' in data:
|
|
response.data = {**data, 'trace_id': trace_id}
|
|
return response
|
|
|
|
# DRF ValidationError: {'field': ['error msg']}
|
|
if isinstance(exc, ValidationError):
|
|
response.data = {
|
|
'code': 'VALIDATION_ERROR',
|
|
'message': '请求参数不合法',
|
|
'details': data,
|
|
'trace_id': trace_id,
|
|
}
|
|
return response
|
|
|
|
# Standard DRF exceptions (AuthenticationFailed, PermissionDenied, Throttled, etc.)
|
|
message = _extract_message(data)
|
|
response.data = {
|
|
'code': _STATUS_TO_CODE.get(status_code, 'SYS_INTERNAL'),
|
|
'message': message,
|
|
'details': None,
|
|
'trace_id': trace_id,
|
|
}
|
|
return response
|
|
|
|
|
|
def _extract_message(data):
|
|
if isinstance(data, dict):
|
|
return str(data.get('detail', data))
|
|
if isinstance(data, list) and data:
|
|
return str(data[0])
|
|
return str(data)
|
|
|
|
|
|
class AppError(APIException):
|
|
"""统一业务异常。视图中 raise AppError('CODE', '消息', details, status_code) 即可。"""
|
|
|
|
def __init__(self, code, message, details=None, status_code=400):
|
|
self.status_code = status_code
|
|
super().__init__(detail=code)
|
|
# 绕过 DRF _get_error_details,防止 None 被转成字符串 "None"
|
|
self.detail = {'code': code, 'message': message, 'details': details}
|