diff --git a/backend/src/platform/api/middleware.py b/backend/src/platform/api/middleware.py index a19e61e..d364e6f 100644 --- a/backend/src/platform/api/middleware.py +++ b/backend/src/platform/api/middleware.py @@ -3,6 +3,7 @@ import logging import time +from starlette.exceptions import HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response, JSONResponse @@ -61,6 +62,8 @@ async def dispatch(self, request: Request, call_next) -> Response: {"detail": str(exc)}, status_code=status.HTTP_503_SERVICE_UNAVAILABLE, ) + except HTTPException: + raise # Let Starlette handle route-level HTTP errors (e.g. 404) except Exception: logger.exception("Unhandled exception in PlatformMiddleware") return JSONResponse( diff --git a/backend/src/platform/api/routes.py b/backend/src/platform/api/routes.py index a98fe03..7ba1011 100644 --- a/backend/src/platform/api/routes.py +++ b/backend/src/platform/api/routes.py @@ -407,7 +407,11 @@ async def init_environment(request: Request) -> JSONResponse: logger.warning("Unauthorized template access in init_environment") return unauthorized() except ValueError as e: - logger.warning(f"Template resolution failed in init_environment: {e}") + logger.warning( + f"Template resolution failed in init_environment: {e} " + f"(service={body.templateService!r}, name={body.templateName!r}, " + f"testId={body.testId!r}, schema={body.templateSchema!r})" + ) return bad_request(str(e)) if not body.testId and not body.impersonateUserId and not body.impersonateEmail: diff --git a/backend/src/platform/isolationEngine/environment.py b/backend/src/platform/isolationEngine/environment.py index 010cd27..a7326ad 100644 --- a/backend/src/platform/isolationEngine/environment.py +++ b/backend/src/platform/isolationEngine/environment.py @@ -55,14 +55,35 @@ def migrate_schema(self, template_schema: str, target_schema: str) -> None: self._set_replica_identity(target_schema) def _ensure_box_columns(self, schema: str) -> None: - """Add columns that may be missing from older template snapshots.""" + """Add columns that may be missing from older template snapshots. + + Only runs against schemas that actually contain Box tables. Each + ALTER TABLE is wrapped in a SAVEPOINT so that a single failure does + not poison the surrounding transaction (the same pattern used by + ``_copy_custom_indexes``). + """ _columns = [ # (table, column, SQL type, default) ("box_folders", "path", "VARCHAR(500)", "'/'"), ("box_files", "path", "VARCHAR(500)", "'/0/'"), ] with self.session_manager.base_engine.begin() as conn: + # Quick check: skip entirely when the schema has no Box tables. + has_box = conn.execute( + text( + "SELECT EXISTS(" + " SELECT 1 FROM information_schema.tables" + " WHERE table_schema = :schema" + " AND table_name = 'box_folders'" + ")" + ), + {"schema": schema}, + ).scalar() + if not has_box: + return + for table, col, sql_type, default in _columns: + nested = conn.begin_nested() try: conn.execute( text( @@ -71,7 +92,9 @@ def _ensure_box_columns(self, schema: str) -> None: f"DEFAULT {default}" ) ) + nested.commit() except Exception as exc: + nested.rollback() logger.warning( f"Could not ensure column {schema}.{table}.{col}: {exc}" )