""" __init__.py - auther: tanlie date: 2026/8/30 """ import logging import os from logging.handlers import RotatingFileHandler import traceback import re from fastapi import FastAPI, Request, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from app.core.redis_lifespan_fixed import lifespan from app.mobile import register_mobile_routes from app.core.response import error_response from app.core.logger import logger def _clean_message(s: str) -> str: """Normalize exception messages by removing wrapper prefixes like "ValueError,".""" if not s: return s return re.sub(r'^\s*(?:ValueError|Value\s*error)[:,]?\s*', '', str(s), flags=re.I) def create_app(): app = FastAPI(lifespan=lifespan) # Attach a DB session to each request via middleware so handlers can use request.state.db from app.core.db import SessionLocal @app.middleware("http") async def db_session_middleware(request: Request, call_next): # Provide a request-scoped SQLAlchemy session request.state.db = SessionLocal() try: response = await call_next(request) return response except Exception: # If an unhandled exception occurred, rollback any pending transaction try: request.state.db.rollback() except Exception: pass raise finally: request.state.db.close() register_mobile_routes(app) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): # Build a friendly, JSON-serializable summary of validation errors errors = [] for e in exc.errors(): loc = e.get("loc", []) # remove leading location indicators like 'body', 'query', 'path', 'header' if loc and len(loc) > 0 and str(loc[0]) in ("body", "query", "path", "header"): loc_parts = loc[1:] else: loc_parts = loc # join remaining location parts into a dotted path for readability loc_str = ".".join([str(p) for p in loc_parts if p is not None]) if loc_parts else "" cleaned_msg = _clean_message(e.get("msg", "")) errors.append({"field": loc_str, "message": cleaned_msg}) # Log validation details for debugging logger.info("Validation error for %s: %s", request.url.path, errors) return JSONResponse( status_code=422, content=error_response(msg="参数校验失败", code=422, data=errors), ) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): # Use HTTPException's status_code and detail detail = exc.detail if exc.detail is not None else "" logger.warning("HTTPException for %s: %s", request.url.path, detail) return JSONResponse( status_code=getattr(exc, "status_code", 500), content=error_response(msg=str(detail), code=getattr(exc, "status_code", 500)), ) @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): # Map common ValueError to a friendlier 400 parameter error if isinstance(exc, ValueError): # Log with stack for debugging tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) raw_msg = str(exc) or "" cleaned_msg = _clean_message(raw_msg) logger.warning("ValueError on %s: %s\n%s", request.url.path, cleaned_msg, tb) return JSONResponse( status_code=400, content=error_response(msg=cleaned_msg or "参数错误", code=400), ) # Generic fallback to avoid leaking internal stack traces # Log full stack trace for internal debugging tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) logger.error("Unhandled exception on %s: %s\n%s", request.url.path, str(exc), tb) return JSONResponse( status_code=500, content=error_response(msg="服务器内部错误", code=500), ) return app