__init__.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. """
  2. __init__.py -
  3. auther: tanlie
  4. date: 2026/8/30
  5. """
  6. import logging
  7. import os
  8. from logging.handlers import RotatingFileHandler
  9. import traceback
  10. import re
  11. from fastapi import FastAPI, Request, HTTPException
  12. from fastapi.exceptions import RequestValidationError
  13. from fastapi.responses import JSONResponse
  14. from app.core.redis_lifespan_fixed import lifespan
  15. from app.mobile import register_mobile_routes
  16. from app.core.response import error_response
  17. from app.core.logger import logger
  18. def _clean_message(s: str) -> str:
  19. """Normalize exception messages by removing wrapper prefixes like "ValueError,"."""
  20. if not s:
  21. return s
  22. return re.sub(r'^\s*(?:ValueError|Value\s*error)[:,]?\s*', '', str(s), flags=re.I)
  23. def create_app():
  24. app = FastAPI(lifespan=lifespan)
  25. # Attach a DB session to each request via middleware so handlers can use request.state.db
  26. from app.core.db import SessionLocal
  27. @app.middleware("http")
  28. async def db_session_middleware(request: Request, call_next):
  29. # Provide a request-scoped SQLAlchemy session
  30. request.state.db = SessionLocal()
  31. try:
  32. response = await call_next(request)
  33. return response
  34. except Exception:
  35. # If an unhandled exception occurred, rollback any pending transaction
  36. try:
  37. request.state.db.rollback()
  38. except Exception:
  39. pass
  40. raise
  41. finally:
  42. request.state.db.close()
  43. register_mobile_routes(app)
  44. @app.exception_handler(RequestValidationError)
  45. async def validation_exception_handler(request: Request, exc: RequestValidationError):
  46. # Build a friendly, JSON-serializable summary of validation errors
  47. errors = []
  48. for e in exc.errors():
  49. loc = e.get("loc", [])
  50. # remove leading location indicators like 'body', 'query', 'path', 'header'
  51. if loc and len(loc) > 0 and str(loc[0]) in ("body", "query", "path", "header"):
  52. loc_parts = loc[1:]
  53. else:
  54. loc_parts = loc
  55. # join remaining location parts into a dotted path for readability
  56. loc_str = ".".join([str(p) for p in loc_parts if p is not None]) if loc_parts else ""
  57. cleaned_msg = _clean_message(e.get("msg", ""))
  58. errors.append({"field": loc_str, "message": cleaned_msg})
  59. # Log validation details for debugging
  60. logger.info("Validation error for %s: %s", request.url.path, errors)
  61. return JSONResponse(
  62. status_code=422,
  63. content=error_response(msg="参数校验失败", code=422, data=errors),
  64. )
  65. @app.exception_handler(HTTPException)
  66. async def http_exception_handler(request: Request, exc: HTTPException):
  67. # Use HTTPException's status_code and detail
  68. detail = exc.detail if exc.detail is not None else ""
  69. logger.warning("HTTPException for %s: %s", request.url.path, detail)
  70. return JSONResponse(
  71. status_code=getattr(exc, "status_code", 500),
  72. content=error_response(msg=str(detail), code=getattr(exc, "status_code", 500)),
  73. )
  74. @app.exception_handler(Exception)
  75. async def generic_exception_handler(request: Request, exc: Exception):
  76. # Map common ValueError to a friendlier 400 parameter error
  77. if isinstance(exc, ValueError):
  78. # Log with stack for debugging
  79. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  80. raw_msg = str(exc) or ""
  81. cleaned_msg = _clean_message(raw_msg)
  82. logger.warning("ValueError on %s: %s\n%s", request.url.path, cleaned_msg, tb)
  83. return JSONResponse(
  84. status_code=400,
  85. content=error_response(msg=cleaned_msg or "参数错误", code=400),
  86. )
  87. # Generic fallback to avoid leaking internal stack traces
  88. # Log full stack trace for internal debugging
  89. tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
  90. logger.error("Unhandled exception on %s: %s\n%s", request.url.path, str(exc), tb)
  91. return JSONResponse(
  92. status_code=500,
  93. content=error_response(msg="服务器内部错误", code=500),
  94. )
  95. return app