Files
NexaPG/backend/app/main.py
nessi 9aecbea68b Add consistent API error handling and documentation
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.
2026-02-13 17:30:05 +01:00

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)