"""统一 API 响应外壳(Wave 0 ok/fail + 风控模块 error_body/register_error_handlers)。""" from __future__ import annotations from typing import Any from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException from app.model.schemas import ApiResponse from app.utils.exceptions import ApiError, PermissionDenied from app.utils.trace import current_request_id, current_trace, new_request_id, new_trace _HTTP_ERROR_CODES = {404: "NOT_FOUND", 405: "METHOD_NOT_ALLOWED"} def ok(data: Any, trace_id: str, message: str = "ok") -> ApiResponse: return ApiResponse(code=0, message=message, data=data, trace_id=trace_id) def fail(code: int, message: str, trace_id: str, *, data: Any = None) -> ApiResponse: return ApiResponse(code=code, message=message, data=data, trace_id=trace_id) def error_body(error_code: str, message: str) -> dict[str, str]: 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: @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"), ) def trace_headers(request: Request) -> dict[str, str]: """从 request.state 取 trace/request 头(异常路径 contextvar 可能已复位)。""" trace_id = getattr(request.state, "trace_id", None) or current_trace() or new_trace() request_id = getattr(request.state, "request_id", None) or current_request_id() or new_request_id() return {"X-Trace-Id": trace_id, "X-Request-Id": request_id}