"""FastAPI composition root: wiring, lifespan, middleware, routers.""" from collections.abc import AsyncIterator from contextlib import asynccontextmanager from fastapi import FastAPI, WebSocket from fastapi.middleware.cors import CORSMiddleware from app.api.errors import register_exception_handlers from app.api.health import router as health_router from app.api.middleware import CorrelationIdMiddleware from app.api.rest import subsonic_router from app.api.v1 import api_v1_router from app.core.config import app_version, get_settings from app.core.logging import configure_logging, get_logger from app.infrastructure.cache import close_redis from app.infrastructure.db import dispose_engine log = get_logger(__name__) @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: settings = get_settings() log.info("startup", environment=settings.environment) yield log.info("shutdown") await dispose_engine() await close_redis() def create_app() -> FastAPI: settings = get_settings() configure_logging(level=settings.log_level, json=settings.log_json) app = FastAPI( title="mcma-backend", version=app_version(), summary="Self-hosted, offline-first music service.", lifespan=lifespan, ) app.add_middleware(CorrelationIdMiddleware) # CORS added last → outermost, so browser preflight (OPTIONS) is answered # before anything else. The web UI can connect cross-origin (direct :8000, # a LAN IP, 127.0.0.1 vs localhost), which needs these headers. Bearer-token # auth (no cookies) → wildcard origins are safe with allow_credentials=False. if settings.cors_allow_origins: app.add_middleware( CORSMiddleware, allow_origins=settings.cors_allow_origins, allow_credentials=False, allow_methods=["*"], allow_headers=["*"], expose_headers=["Content-Range", "Accept-Ranges", "Content-Length"], ) register_exception_handlers(app) app.include_router(health_router) app.include_router(api_v1_router) app.include_router(subsonic_router, prefix="/rest") @app.websocket("/ws") async def ws_stub(websocket: WebSocket) -> None: await websocket.accept() await websocket.close(1001) return app app = create_app()