Files
XingHuo/app/main.py
T

108 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FastAPI 入口(B7 集成 · T-01/T-02 演进):路由挂载、trace 中间件、审计中间件、lifespan、统一错误体。
lifespan(B7 挂账⑤⑥):
- 启动期校验:非 development 环境须 JWT 就绪(RS256 公钥已配置;
T-01 前的 debug 工厂在场同样拒绝——AUTH_FACTORY_IS_DEBUG 双保险);
- Redis 网关单例注册(惰性连接,publish/DEL/EXISTS 失败降级不阻塞业务);
- shutdown 统一 dispose 数据库引擎(utils/db 工厂,B6 复审 P3 泄漏收口)。
trace:X-Trace-Id 透传/生成 + 响应头回写(trace.py 约束:call_next 前 set,
同步路由线程池由 anyio 传播,一致性 B8 断言兜底)。
audit(T-02):平台访问审计(http_access)+ input_guard_log 双写见
audit_middleware;X-Request-Id 独立生成(B7 复审 P3-4)。
"""
from __future__ import annotations
import logging
import re
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.api import deps
from app.api.audit_middleware import audit_middleware
from app.api.risk import router as risk_router
from app.api.simulate import router as simulate_router
from app.config.settings import settings
from app.service.auth_service import jwt_ready
from app.service.risk import redis_gateway
from app.utils.db import dispose_engines
from app.utils.response import error_body, register_error_handlers
from app.utils.trace import (
bind_request_id,
new_trace,
reset_request_id,
reset_trace,
set_trace,
)
logger = logging.getLogger(__name__)
# 透传外部 X-Trace-Id 的格式白名单(防响应头注入;不合规一律新生成)
_TRACE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
@asynccontextmanager
async def lifespan(_: FastAPI):
if settings.app_env != "development":
if deps.AUTH_FACTORY_IS_DEBUG:
raise RuntimeError(
"debug auth factory is wired but app_env is not 'development'; "
"deploy T-01 JWT auth first or set app_env=development"
)
reason = jwt_ready()
if reason:
raise RuntimeError(f"JWT auth not ready for non-development env: {reason}")
redis_gateway.set_gateway(redis_gateway.RedisGateway())
try:
yield
finally:
redis_gateway.set_gateway(None)
dispose_engines()
app = FastAPI(title="JinRong Agent Platform", version="0.2.0", lifespan=lifespan)
register_error_handlers(app)
app.include_router(risk_router)
app.include_router(simulate_router)
@app.middleware("http")
async def audit_middleware_entry(request: Request, call_next):
"""T-02 访问审计:先注册(执行序在 trace 之内,trace_id/request_id 已绑定)。"""
return await audit_middleware(request, call_next)
@app.middleware("http")
async def trace_middleware(request: Request, call_next):
"""trace_id/request_id 贯通 + 未捕获异常兜底(B7 复审 P2-2)。
异常发生在本中间件之内时,Starlette 的 ServerErrorMiddleware(栈外层)
生成的 500 响应不经过用户中间件——trace 头丢失的根因;此处 catch 后
直接产出统一错误体,保证 500 也带 X-Trace-Id/X-Request-Id。
"""
incoming = request.headers.get("X-Trace-Id", "")
trace_id = incoming if _TRACE_ID_PATTERN.fullmatch(incoming) else new_trace()
token = set_trace(trace_id)
request_id, rid_token = bind_request_id(request.headers.get("X-Request-Id", ""))
try:
try:
response = await call_next(request)
except Exception:
logger.exception("unhandled error on %s %s", request.method, request.url.path)
response = JSONResponse(
status_code=500, content=error_body("INTERNAL_ERROR", "internal server error")
)
finally:
reset_trace(token)
reset_request_id(rid_token)
response.headers["X-Trace-Id"] = trace_id
response.headers["X-Request-Id"] = request_id
return response
@app.get("/health")
def health():
return {"status": "ok", "env": settings.app_env}