57 lines
2.8 KiB
Python
57 lines
2.8 KiB
Python
"""统一 API 响应外壳(B7 · 挂账④;T-02 扩展)。
|
||
|
||
成功响应:业务字段平铺不变(B5 评审 P2-2 口径「路由返回体不变」),trace_id
|
||
经 X-Trace-Id 响应头贯通;错误响应统一 JWT 手册 §10 结构
|
||
{error_code, message, trace_id, request_id}——request_id 为独立请求级标识
|
||
(T-02,B7 复审 P3-4 收口),trace_id/request_id 分别对齐响应头。
|
||
|
||
T-02 补齐(B7 复审 P2-2):422 请求校验失败、404/405 路由方法错误改用统一
|
||
错误体(原 FastAPI detail 结构);未捕获异常的 500 由 main.trace 中间件
|
||
兜底(异常穿透 exception handler,直接在中间件层生成错误体并回写 trace 头)。
|
||
main 与测试 app 共用 register_error_handlers。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.exceptions import RequestValidationError
|
||
from fastapi.responses import JSONResponse
|
||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||
|
||
from app.utils.exceptions import ApiError, PermissionDenied
|
||
from app.utils.trace import current_request_id, current_trace, new_trace, new_request_id
|
||
|
||
# 路由级 HTTP 状态 → 错误码(手册 §10 之外的平台通用码)
|
||
_HTTP_ERROR_CODES = {404: "NOT_FOUND", 405: "METHOD_NOT_ALLOWED"}
|
||
|
||
|
||
def error_body(error_code: str, message: str) -> dict[str, str]:
|
||
"""手册 §10 错误体(trace/request 缺失时兜底生成,保证响应可归因)。"""
|
||
tid = current_trace() or new_trace()
|
||
rid = current_request_id() or new_request_id()
|
||
return {"error_code": error_code, "message": message, "trace_id": tid, "request_id": rid}
|
||
|
||
|
||
def register_error_handlers(app: FastAPI) -> None:
|
||
"""注册统一错误体 handler(替代 FastAPI 默认 detail 结构)。"""
|
||
|
||
@app.exception_handler(ApiError)
|
||
async def _api_error_handler(request: Request, exc: ApiError) -> JSONResponse:
|
||
return JSONResponse(status_code=exc.status_code, content=error_body(exc.error_code, exc.message))
|
||
|
||
@app.exception_handler(PermissionDenied)
|
||
async def _permission_denied_handler(request: Request, exc: PermissionDenied) -> JSONResponse:
|
||
return JSONResponse(status_code=403, content=error_body(exc.code, str(exc)))
|
||
|
||
@app.exception_handler(StarletteHTTPException)
|
||
async def _http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
||
code = _HTTP_ERROR_CODES.get(exc.status_code, "HTTP_ERROR")
|
||
return JSONResponse(status_code=exc.status_code, content=error_body(code, str(exc.detail)))
|
||
|
||
@app.exception_handler(RequestValidationError)
|
||
async def _validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||
return JSONResponse(
|
||
status_code=422,
|
||
content=error_body("REQUEST_VALIDATION_FAILED", "request validation failed"),
|
||
)
|