feat: T-02 审计中间件——http_access 全量访问审计(auth 注入 request.state, 排障路径跳过, 写库失败降级) + 独立 request_id(X-Request-Id 白名单透传贯通, B7 复审 P3-4) + 422/404/405/500 统一错误体(500 由 trace 中间件兜底生成并回写 trace 头, B7 复审 P2-2) + input_guard_log 双写(deps 401/403 留痕, platform 网关跳过 ENUM 口径, 挂账⑧收口) + repo.insert_input_guard_log + tests/test_audit_middleware 12 例, 240 绿

This commit is contained in:
2026-09-07 02:32:03 +08:00
parent 36f476f450
commit 3cec034026
9 changed files with 446 additions and 25 deletions
+30 -7
View File
@@ -1,24 +1,35 @@
"""统一 API 响应外壳(B7 · 挂账④)。
"""统一 API 响应外壳(B7 · 挂账④;T-02 扩展)。
成功响应:业务字段平铺不变(B5 评审 P2-2 口径「路由返回体不变」),trace_id
经 X-Trace-Id 响应头贯通(main 中间件);错误响应统一 JWT 手册 §10 结构
{error_code, message, trace_id, request_id}(request_id 沿用 trace_id,
独立请求级标识尚未引入)。main 与测试 app 共用 register_error_handlers。
经 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_trace, new_trace
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 缺失时兜底生成,保证响应可归因)。"""
"""手册 §10 错误体(trace/request 缺失时兜底生成,保证响应可归因)。"""
tid = current_trace() or new_trace()
return {"error_code": error_code, "message": message, "trace_id": tid, "request_id": tid}
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:
@@ -31,3 +42,15 @@ def register_error_handlers(app: FastAPI) -> None:
@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"),
)