Files
social-app/backend/src/app.py
T

173 lines
4.6 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from starlette.exceptions import HTTPException as StarletteHTTPException
from core.config.settings import config
from core.http.response import build_problem_details
from core.logging import configure_logging, get_logger, log_service_banner
from services.base import close_registered_services, initialize_registered_services
from v1.router import router as mobile_router
class HealthResponse(BaseModel):
status: str
configure_logging(config)
log_service_banner(
service_name=config.runtime.service_name,
environment=config.runtime.environment,
)
logger = get_logger("api.app")
SERVICE_STARTUP_ORDER = ["redis", "supabase"]
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
initialized, services = await initialize_registered_services(SERVICE_STARTUP_ORDER)
if not initialized:
logger.error("Service initialization failed, aborting startup")
raise RuntimeError("Service initialization failed")
logger.info("Base services initialized", services=SERVICE_STARTUP_ORDER)
try:
yield
finally:
closed = await close_registered_services(services)
if not closed:
logger.warning("Failed to close all base services")
logger.info("Base services closed", services=SERVICE_STARTUP_ORDER)
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=config.cors.allow_origins,
allow_credentials=config.cors.allow_credentials,
allow_methods=config.cors.allow_methods,
allow_headers=config.cors.allow_headers,
)
app.include_router(mobile_router)
logger.info(
"Web application initialized",
environment=config.runtime.environment,
debug=config.runtime.debug,
log_level=config.runtime.log_level,
)
@app.get("/health", response_model=HealthResponse)
async def health() -> HealthResponse:
return HealthResponse(status="ok")
def _build_http_error_response(
request: Request,
exc: Exception,
status_code: int,
detail: object,
) -> JSONResponse:
instance = request.url.path
detail_text = detail if isinstance(detail, str) else "Request failed"
logger.warning(
"HTTP error",
status_code=status_code,
detail=detail_text,
detail_extra=detail,
path=request.url.path,
method=request.method,
)
problem = build_problem_details(
status_code=status_code,
detail=detail_text,
instance=instance,
)
return JSONResponse(
status_code=status_code,
content=problem.model_dump(),
media_type="application/problem+json",
)
@app.exception_handler(HTTPException)
async def http_exception_handler(
request: Request,
exc: HTTPException,
) -> JSONResponse:
return _build_http_error_response(
request=request,
exc=exc,
status_code=exc.status_code,
detail=exc.detail,
)
@app.exception_handler(StarletteHTTPException)
async def starlette_http_exception_handler(
request: Request,
exc: StarletteHTTPException,
) -> JSONResponse:
return _build_http_error_response(
request=request,
exc=exc,
status_code=exc.status_code,
detail=exc.detail,
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
instance = request.url.path
logger.warning(
"Request validation error",
path=request.url.path,
method=request.method,
errors=exc.errors(),
)
problem = build_problem_details(
status_code=422,
detail="Invalid request",
instance=instance,
)
return JSONResponse(
status_code=422,
content=problem.model_dump(),
media_type="application/problem+json",
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
instance = request.url.path
logger.exception(
"Unhandled error",
path=request.url.path,
method=request.method,
)
problem = build_problem_details(
status_code=500,
detail="Internal Server Error",
instance=instance,
)
return JSONResponse(
status_code=500,
content=problem.model_dump(),
media_type="application/problem+json",
)