Introduced standardized error response formats for API errors, including middleware for consistent request IDs and exception handlers. Updated the frontend to parse and process these error responses, and documented the error format in the README for reference.
129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
import asyncio
|
|
import logging
|
|
from uuid import uuid4
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI, HTTPException, Request
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
from sqlalchemy import select
|
|
from app.api.router import api_router
|
|
from app.core.config import get_settings
|
|
from app.core.db import SessionLocal
|
|
from app.core.errors import error_payload, http_status_to_code
|
|
from app.core.logging import configure_logging
|
|
from app.core.security import hash_password
|
|
from app.models.models import User
|
|
from app.services.collector import collector_loop
|
|
|
|
settings = get_settings()
|
|
configure_logging(settings.log_level)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
collector_task: asyncio.Task | None = None
|
|
collector_stop_event = asyncio.Event()
|
|
|
|
|
|
async def ensure_admin_user() -> None:
|
|
async with SessionLocal() as db:
|
|
admin = await db.scalar(select(User).where(User.email == settings.init_admin_email))
|
|
if admin:
|
|
return
|
|
user = User(
|
|
email=settings.init_admin_email,
|
|
password_hash=hash_password(settings.init_admin_password),
|
|
role="admin",
|
|
)
|
|
db.add(user)
|
|
await db.commit()
|
|
logger.info("created initial admin user")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
global collector_task
|
|
await ensure_admin_user()
|
|
collector_task = asyncio.create_task(collector_loop(collector_stop_event))
|
|
try:
|
|
yield
|
|
finally:
|
|
collector_stop_event.set()
|
|
if collector_task:
|
|
await collector_task
|
|
|
|
|
|
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
|
use_wildcard_cors = settings.cors_origins.strip() == "*"
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"] if use_wildcard_cors else settings.cors_origins_list,
|
|
allow_credentials=False if use_wildcard_cors else True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def request_id_middleware(request: Request, call_next):
|
|
request_id = request.headers.get("x-request-id") or str(uuid4())
|
|
request.state.request_id = request_id
|
|
response = await call_next(request)
|
|
response.headers["X-Request-ID"] = request_id
|
|
return response
|
|
|
|
|
|
@app.exception_handler(HTTPException)
|
|
@app.exception_handler(StarletteHTTPException)
|
|
async def http_exception_handler(request: Request, exc: HTTPException | StarletteHTTPException):
|
|
request_id = getattr(request.state, "request_id", str(uuid4()))
|
|
code = http_status_to_code(exc.status_code)
|
|
message = "Request failed"
|
|
details = None
|
|
|
|
if isinstance(exc.detail, str):
|
|
message = exc.detail
|
|
elif isinstance(exc.detail, dict):
|
|
code = str(exc.detail.get("code", code))
|
|
message = str(exc.detail.get("message", message))
|
|
details = exc.detail.get("details")
|
|
elif isinstance(exc.detail, list):
|
|
message = "Request validation failed"
|
|
details = exc.detail
|
|
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content=error_payload(code=code, message=message, details=details, request_id=request_id),
|
|
)
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
request_id = getattr(request.state, "request_id", str(uuid4()))
|
|
return JSONResponse(
|
|
status_code=422,
|
|
content=error_payload(
|
|
code="validation_error",
|
|
message="Request validation failed",
|
|
details=exc.errors(),
|
|
request_id=request_id,
|
|
),
|
|
)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def unhandled_exception_handler(request: Request, exc: Exception):
|
|
request_id = getattr(request.state, "request_id", str(uuid4()))
|
|
logger.exception("unhandled_exception request_id=%s", request_id, exc_info=exc)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content=error_payload(
|
|
code="internal_error",
|
|
message="Internal server error",
|
|
details=None,
|
|
request_id=request_id,
|
|
),
|
|
)
|
|
|
|
app.include_router(api_router, prefix=settings.api_v1_prefix)
|