优化参数报错提示。

This commit is contained in:
Hommy
2025-09-22 23:32:57 +08:00
parent 53fdeb90a5
commit fdf440b01f
5 changed files with 150 additions and 91 deletions
+2 -2
View File
@@ -25,9 +25,9 @@ class CustomError(Enum):
self.cn_message = cn_message
self.en_message = en_message
def as_dict(self, detail: str = None, language: str = 'zh') -> dict:
def as_dict(self, detail: str = None, lang: str = 'zh') -> dict:
"""转换为API响应格式,支持中英文"""
message = self.cn_message if language == 'zh' else self.en_message
message = self.cn_message if lang == 'zh' else self.en_message
if detail:
message += f"({detail})"
return {"code": self.code, "message": message}
+4 -28
View File
@@ -1,11 +1,7 @@
from fastapi import FastAPI
from src.router import v1_router
from src.utils.logger import logger
from src import middlewares
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi import Request, status
from exceptions import CustomError
from src.middlewares import PrepareMiddleware, ResponseMiddleware
# 0. 创建 FastAPI 应用
@@ -15,31 +11,11 @@ app = FastAPI(title="CapCut Mate API", version="1.0")
app.include_router(v1_router, prefix="/openapi", tags=["capcut-mate"])
# 2. 添加中间件
app.middleware("http")(middlewares.prepare_middleware)
app.add_middleware(PrepareMiddleware)
# 注册统一响应处理中间件(注意顺序,应该在其他中间件之后注册)
app.middleware("http")(middlewares.response_middleware)
app.add_middleware(ResponseMiddleware)
# 3. 异常处理:参数校验错误
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
"""自定义参数校验错误处理器"""
# 1. 拼接完整错误信息
error_messages = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
error_type = error["type"]
error_msg = error["msg"]
# 组合字段+错误类型+详情信息
error_messages.append(f"{field} ({error_type}): {error_msg}")
# 2. 构建统一响应结构
full_message = "; ".join(error_messages) # 用分号分隔多个错误
return JSONResponse(
status_code=status.HTTP_200_OK,
content=CustomError.PARAM_VALIDATION_FAILED.as_dict(full_message)
)
# 4. 打印所有路由
# 3. 打印所有路由
for r in app.routes:
# 1. 取 HTTP 方法列表
methods = getattr(r, "methods", None) or [getattr(r, "method", "WS")]
+3 -3
View File
@@ -1,5 +1,5 @@
# 中间件实现
from .prepare import prepare_middleware
from .response import response_middleware
from .prepare import PrepareMiddleware
from .response import ResponseMiddleware
__all__ = ["prepare_middleware", "response_middleware"]
__all__ = ["PrepareMiddleware", "ResponseMiddleware"]
+14 -7
View File
@@ -1,14 +1,21 @@
# 环境初始化中间件
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
import config
import os
# 请求前的准备工作,如:创建草稿目录
async def prepare_middleware(request: Request, call_next):
# 递归创建目录,如果目录存在,就直接跳过创建
os.makedirs(config.DRAFT_DIR, exist_ok=True)
class PrepareMiddleware(BaseHTTPMiddleware):
"""请求前的准备工作中间件
功能:
1. 创建临时目录
2. 创建输出目录
"""
# 继续处理请求
response = await call_next(request)
return response
async def dispatch(self, request: Request, call_next):
# 递归创建目录,如果目录存在,就直接跳过创建
os.makedirs(config.DRAFT_DIR, exist_ok=True)
# 继续处理请求
response = await call_next(request)
return response
+127 -51
View File
@@ -1,65 +1,141 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from exceptions import CustomError, CustomException
from starlette.middleware.base import BaseHTTPMiddleware
from src.utils.logger import logger
import json
async def response_middleware(request: Request, call_next):
class ResponseMiddleware(BaseHTTPMiddleware):
"""统一响应处理中间件
功能:
1. 统一处理业务正常响应,添加code和message字段
2. 统一处理异常,返回标准错误格式
"""
try:
# 获取语言偏好(默认中文)
lang = request.headers.get('Accept-Language', 'zh').split(',')[0].split('-')[0]
if lang not in ['zh', 'en']:
lang = 'zh'
# 调用下一个处理函数
response = await call_next(request)
# 检查是否为JSON响应
if response.headers.get('content-type') == 'application/json':
# 读取响应内容
body = [section async for section in response.body_iterator]
if body:
body_str = b''.join(body).decode()
try:
# 解析JSON响应
data = json.loads(body_str)
# 检查响应是否已经包含code和message
if 'code' not in data or 'message' not in data:
# 包装成统一响应格式(平铺业务数据)
unified_response = {
'code': CustomError.SUCCESS.code,
'message': CustomError.SUCCESS.as_dict(language=lang)['message'],
**data
}
# 创建新的JSON响应
return JSONResponse(
status_code=response.status_code,
content=unified_response
)
except json.JSONDecodeError:
logger.warning(f"JSON decode error: {body_str}")
async def dispatch(self, request: Request, call_next):
try:
lang = self._get_language_from_request(request)
response = await call_next(request)
# 处理非200状态码的响应
if response.status_code != 200:
return await self._handle_non_200_response(response, lang)
# 处理JSON响应
if self._is_json_response(response):
return await self._process_json_response(response, lang)
return response
return response
except CustomException as e:
return self._handle_custom_exception(e, lang)
except Exception as e:
return self._handle_generic_exception(e, lang)
def _get_language_from_request(self, request: Request) -> str:
"""从请求头获取语言偏好"""
lang = request.headers.get('Accept-Language', 'zh').split(',')[0].split('-')[0]
return lang if lang in ['zh', 'en'] else 'zh'
def _handle_422_error(self, body_str: str, lang: str) -> JSONResponse:
"""特殊处理422参数验证错误"""
try:
# 尝试解析422错误的响应体
error_data = json.loads(body_str)
# 提取验证错误的详细信息
validation_messages = []
if "detail" in error_data:
for error in error_data["detail"]:
if "loc" in error and "msg" in error:
# 格式化错误信息
field = ".".join(str(part) for part in error["loc"] if part != "body")
message = f"{field}: {error['msg']}"
validation_messages.append(message)
# 构建统一的422错误响应(不包含data字段)
error_message = "; ".join(validation_messages) if validation_messages else ""
error_response = CustomError.PARAM_VALIDATION_FAILED.as_dict(detail=error_message, lang=lang)
return JSONResponse(status_code=200, content=error_response)
except json.JSONDecodeError:
logger.warning(f"Failed to parse 422 response body: {body_str}")
error_response = CustomError.PARAM_VALIDATION_FAILED.as_dict(detail=body_str, lang=lang)
return JSONResponse(status_code=200, content=error_response)
async def _handle_non_200_response(self, response, lang: str) -> JSONResponse:
"""处理非200状态码的响应"""
body = b""
async for chunk in response.body_iterator:
body += chunk
except CustomException as e:
# 处理自定义业务异常
logger.error(f"Custom exception: {e.err.code} - {e.err.cn_message}" + (f" ({e.detail})" if e.detail else ""))
return JSONResponse(
status_code=200,
content=e.err.as_dict(e.detail, language=lang)
)
except Exception as e:
# 处理其他未捕获的异常
logger.error(f"Internal server error: {str(e)}")
return JSONResponse(
status_code=200,
content=CustomError.INTERNAL_SERVER_ERROR.as_dict(str(e), language=lang)
)
body_str = body.decode()
# 特殊处理422错误(参数验证错误)
if response.status_code == 422:
return self._handle_422_error(body_str, lang)
# 其它情况不应该发生,每一个错误都应该在前面被处理
logger.error(f"Non-200 response: {response.status_code} - {body_str}")
# 其他非200错误处理
error_response = {
"code": response.status_code,
"message": f"HTTP Error {response.status_code}",
"data": {"detail": body_str}
}
return JSONResponse(status_code=200, content=error_response)
def _is_json_response(self, response) -> bool:
"""检查是否为JSON响应"""
return response.headers.get('content-type') == 'application/json'
async def _process_json_response(self, response, lang: str):
"""处理JSON响应并统一格式"""
body = [section async for section in response.body_iterator]
if not body:
return response
body_str = b''.join(body).decode()
try:
data = json.loads(body_str)
# 如果响应已经有统一格式,直接返回
if 'code' in data and 'message' in data:
return response
# 创建统一格式的响应(成功响应保留data字段)
unified_response = {
'code': CustomError.SUCCESS.code,
'message': CustomError.SUCCESS.as_dict(lang=lang)['message'],
'data': data
}
return JSONResponse(
status_code=response.status_code,
content=unified_response
)
except json.JSONDecodeError:
logger.warning(f"JSON decode error: {body_str}")
return response
def _handle_custom_exception(self, e: CustomException, lang: str) -> JSONResponse:
"""处理自定义异常(不包含data字段)"""
logger.warning(f"Custom exception: {e.err.code} - {e.err.cn_message}" +
(f" ({e.detail})" if e.detail else ""))
# 获取错误信息
error_response = e.err.as_dict(detail=e.detail, lang=lang)
return JSONResponse(status_code=200, content=error_response)
def _handle_generic_exception(self, e: Exception, lang: str) -> JSONResponse:
"""处理通用异常(不包含data字段)"""
logger.warning(f"Internal server error: {str(e)}")
# 获取错误信息
error_response = CustomError.INTERNAL_SERVER_ERROR.as_dict(detail=str(e), lang=lang)
return JSONResponse(status_code=200, content=error_response)