- 添加 .env.example 环境变量配置示例 - 添加 .gitignore 忽略文件配置 - 添加 core/config.py 配置管理模块 - 添加 deployments/k8s/configmap.yaml Kubernetes 配置 - 添加 core/database.py 数据库连接管理模块 - 添加 core/dependencies.py 全局依赖模块 - 添加 DEPENDENCIES_UPDATED.md 依赖更新记录 - 添加 deployments/k8s/deployment.yaml Kubernetes 部署配置- 添加 deployments/swarm/docker-compose.swarm.yml Docker Swarm 部署配置 - 添加 deployments/docker/docker-compose.yml Docker 部署配置 - 添加 deployments/docker/Dockerfile 应用镜像构建文件 - 添加 middleware/error_handler.py 全局异常处理中间件
220 lines
5.6 KiB
Python
220 lines
5.6 KiB
Python
"""
|
|
Global exception handler middleware.
|
|
Catches all exceptions and returns standardized error responses.
|
|
"""
|
|
|
|
import traceback
|
|
from typing import Union
|
|
from fastapi import Request, status
|
|
from fastapi.responses import ORJSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
from pydantic import ValidationError
|
|
from core.exceptions import BaseAppException
|
|
from core.responses import error, BusinessCode, ErrorMessage
|
|
from observability.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
async def app_exception_handler(
|
|
request: Request,
|
|
exc: BaseAppException
|
|
) -> ORJSONResponse:
|
|
"""
|
|
Handle application-specific exceptions.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
exc: Application exception
|
|
|
|
Returns:
|
|
JSONResponse: Standardized error response
|
|
"""
|
|
# Get trace ID from request
|
|
trace_id = getattr(request.state, "trace_id", "")
|
|
|
|
# Log exception
|
|
logger.error(
|
|
f"Application error: {exc.message}",
|
|
extra={
|
|
"trace_id": trace_id,
|
|
"error_code": exc.code,
|
|
"details": exc.details,
|
|
},
|
|
exc_info=True
|
|
)
|
|
|
|
# Return error response
|
|
error_response = error(
|
|
code=exc.code,
|
|
message=exc.message,
|
|
trace_id=trace_id
|
|
)
|
|
|
|
return ORJSONResponse(
|
|
status_code=exc.status_code,
|
|
content=error_response.model_dump(mode='json')
|
|
)
|
|
|
|
|
|
async def validation_exception_handler(
|
|
request: Request,
|
|
exc: Union[RequestValidationError, ValidationError]
|
|
) -> ORJSONResponse:
|
|
"""
|
|
Handle Pydantic validation errors.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
exc: Validation error
|
|
|
|
Returns:
|
|
JSONResponse: Standardized error response
|
|
"""
|
|
# Get trace ID from request
|
|
trace_id = getattr(request.state, "trace_id", "")
|
|
|
|
# Extract validation errors
|
|
errors = exc.errors() if hasattr(exc, "errors") else []
|
|
|
|
# Format error message
|
|
if errors:
|
|
first_error = errors[0]
|
|
field = ".".join(str(loc) for loc in first_error.get("loc", []))
|
|
message = f"Validation error: {field} - {first_error.get('msg', 'Invalid value')}"
|
|
else:
|
|
message = "Validation error"
|
|
|
|
# Log validation error
|
|
logger.warning(
|
|
f"Validation error: {message}",
|
|
extra={
|
|
"trace_id": trace_id,
|
|
"validation_errors": errors,
|
|
}
|
|
)
|
|
|
|
# Return error response
|
|
error_response = error(
|
|
code=BusinessCode.INVALID_INPUT,
|
|
message=message,
|
|
trace_id=trace_id
|
|
)
|
|
|
|
return ORJSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content=error_response.model_dump(mode='json')
|
|
)
|
|
|
|
|
|
async def http_exception_handler(
|
|
request: Request,
|
|
exc: StarletteHTTPException
|
|
) -> ORJSONResponse:
|
|
"""
|
|
Handle HTTP exceptions.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
exc: HTTP exception
|
|
|
|
Returns:
|
|
JSONResponse: Standardized error response
|
|
"""
|
|
# Get trace ID from request
|
|
trace_id = getattr(request.state, "trace_id", "")
|
|
|
|
# Map HTTP status to business code
|
|
code_mapping = {
|
|
404: BusinessCode.RESOURCE_NOT_FOUND,
|
|
401: BusinessCode.LOGIN_FAILED,
|
|
403: BusinessCode.INSUFFICIENT_PERMISSIONS,
|
|
409: BusinessCode.RESOURCE_CONFLICT,
|
|
}
|
|
|
|
business_code = code_mapping.get(exc.status_code, BusinessCode.UNKNOWN_ERROR)
|
|
|
|
# Log HTTP error
|
|
logger.warning(
|
|
f"HTTP error {exc.status_code}: {exc.detail}",
|
|
extra={
|
|
"trace_id": trace_id,
|
|
"status_code": exc.status_code,
|
|
}
|
|
)
|
|
|
|
# Return error response
|
|
error_response = error(
|
|
code=business_code,
|
|
message=str(exc.detail),
|
|
trace_id=trace_id
|
|
)
|
|
|
|
return ORJSONResponse(
|
|
status_code=exc.status_code,
|
|
content=error_response.model_dump(mode='json')
|
|
)
|
|
|
|
|
|
async def generic_exception_handler(
|
|
request: Request,
|
|
exc: Exception
|
|
) -> ORJSONResponse:
|
|
"""
|
|
Handle all other unhandled exceptions.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
exc: Any exception
|
|
|
|
Returns:
|
|
JSONResponse: Standardized error response
|
|
"""
|
|
# Get trace ID from request
|
|
trace_id = getattr(request.state, "trace_id", "")
|
|
|
|
# Log full exception with traceback
|
|
logger.error(
|
|
f"Unhandled exception: {str(exc)}",
|
|
extra={
|
|
"trace_id": trace_id,
|
|
"exception_type": type(exc).__name__,
|
|
"traceback": traceback.format_exc(),
|
|
},
|
|
exc_info=True
|
|
)
|
|
|
|
# Return generic error response
|
|
error_response = error(
|
|
code=BusinessCode.UNKNOWN_ERROR,
|
|
message=ErrorMessage.UNKNOWN_ERROR,
|
|
trace_id=trace_id
|
|
)
|
|
|
|
return ORJSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content=error_response.model_dump(mode='json')
|
|
)
|
|
|
|
|
|
def register_exception_handlers(app) -> None:
|
|
"""
|
|
Register all exception handlers with FastAPI application.
|
|
|
|
Args:
|
|
app: FastAPI application instance
|
|
"""
|
|
# Application exceptions
|
|
app.add_exception_handler(BaseAppException, app_exception_handler)
|
|
|
|
# Validation errors
|
|
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
app.add_exception_handler(ValidationError, validation_exception_handler)
|
|
|
|
# HTTP exceptions
|
|
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
|
|
|
|
# Generic exception handler (catch-all)
|
|
app.add_exception_handler(Exception, generic_exception_handler)
|