From 80b3a10db8072d0542ab3c53a92e59b725c6fa8e Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 21 Apr 2026 21:41:20 +0200 Subject: [PATCH] Rebind TUS routes and Galaxy middleware on cached fast app reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded-test FastAPI cache reused the first test class's: - TUS routes, which ``create_tus_router`` bakes ``files_dir`` / ``max_size`` into at build time — pointing at the first test's ``tus_upload_store`` temp directory, which is removed on teardown. Later tests' TUS uploads failed, surfacing as workflow/upload selenium failures and direct TUS test breakage. - ``GalaxyCORSMiddleware`` / ``XFrameOptionsMiddleware``, which capture ``gx_app.config`` at ``add_middleware`` time — leaving later tests enforcing the first test's ``allowed_origin_hostnames`` / ``x_frame_options`` and breaking CORS integration tests. Rebind both on each cached-app reuse: strip the old TUS routes (while preserving the root WSGI mount's trailing position), re-include fresh ones bound to the current ``gx_app``'s upload store, and re-run ``add_galaxy_middleware`` after evicting the old Galaxy middleware entries from ``app.user_middleware``. Reset ``middleware_stack`` so Starlette rebuilds it on the next request, and refresh ``route_name_index`` to reflect the replaced TUS routes. ``add_galaxy_middleware`` also re-applies ``GalaxyFileResponse`` sendfile attributes, so the explicit re-assignment in ``_rebind_fast_app_for_launch`` is now redundant and removed. --- lib/galaxy_test/driver/driver_util.py | 70 ++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index 5af1e873c9e..c171a446349 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -44,11 +44,15 @@ from galaxy.util import ( galaxy_directory, ) from galaxy.util.properties import load_app_properties -from galaxy.webapps.base.api import GalaxyFileResponse +from galaxy.webapps.base.api import build_route_name_index from galaxy.webapps.galaxy import buildapp from galaxy.webapps.galaxy.fast_app import ( _build_merged_openapi, + add_galaxy_middleware, + GalaxyCORSMiddleware, + include_tus, initialize_fast_app as init_galaxy_fast_app, + XFrameOptionsMiddleware, ) from galaxy_test.base.api_util import ( get_admin_api_key, @@ -778,19 +782,72 @@ def _find_root_wsgi_mount(app: FastAPI) -> Optional[Mount]: return None +_TUS_PREFIXES = ( + "/api/upload/resumable_upload", + "/api/job_files/resumable_upload", +) + + +def _rebind_tus_routes(app: FastAPI, gx_app) -> None: + """Replace TUS routes so they point at the current launch's upload store. + + ``create_tus_router`` bakes ``files_dir`` / ``max_size`` into the route + handlers at build time, so the cached app's TUS routes would otherwise + keep writing to the first launch's ``tus_upload_store`` — a temp + directory that's removed when that test class tears down. + """ + root_mount = _find_root_wsgi_mount(app) + tus_routes = [r for r in app.router.routes if _is_tus_route(r)] + for r in tus_routes: + app.router.routes.remove(r) + if root_mount is not None: + app.router.routes.remove(root_mount) + include_tus(app, gx_app) + if root_mount is not None: + app.router.routes.append(root_mount) + + +def _is_tus_route(route) -> bool: + path = getattr(route, "path", None) + return bool(path and any(path.startswith(p) for p in _TUS_PREFIXES)) + + +def _rebind_galaxy_middleware(app: FastAPI, gx_app) -> None: + """Re-run ``add_galaxy_middleware`` against the current gx_app. + + CORS / X-Frame-Options middleware capture ``gx_app.config`` at the + moment they're added to ``app.user_middleware``; without this the + cached app would keep enforcing the first test's + ``allowed_origin_hostnames`` / ``x_frame_options`` for every + subsequent test. + """ + app.user_middleware = [ + mw for mw in app.user_middleware if mw.cls not in (GalaxyCORSMiddleware, XFrameOptionsMiddleware) + ] + add_galaxy_middleware(app, gx_app) + # Force Starlette to rebuild its middleware stack on the next request + # so the re-added middleware actually takes effect. + app.middleware_stack = None + + def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: """Re-bind the per-launch pieces of a cached FastAPI app. Galaxy routes resolve the current ``gx_app`` via the module global ``galaxy.app.app`` (set by ``buildapp.app_pair`` on every launch), so - the app's routes/middleware/route-name index are app-agnostic and can - be shared across repeated embedded-server launches within one Python - process (i.e. pytest). The only pieces that must be refreshed: + most of the app's routes are app-agnostic and can be shared across + repeated embedded-server launches within one Python process (i.e. + pytest). The pieces that must be refreshed: - the WSGI middleware wrapping this launch's paste webapp and its executor shutdown on ``gx_app.haltables``; + - the TUS routes, which bake this launch's ``tus_upload_store`` + directory into their route handlers; + - Galaxy-specific middleware (CORS, X-Frame-Options), which capture + this launch's ``gx_app.config`` at add time; - the ``GalaxyFileResponse`` sendfile-mode class attributes (which ``add_galaxy_middleware`` wrote from the first build's ``gx_app``); + - the route-name index, since TUS routes were just replaced; - the merged OpenAPI schema, rebuilt lazily against the current ``gx_app`` if ``/openapi.json`` is requested (integration tests don't hit it, so eagerly rebuilding it per rebind would just add @@ -802,8 +859,9 @@ def _rebind_fast_app_for_launch(app: FastAPI, gx_wsgi_webapp, gx_app) -> None: new_wsgi_handler = WSGIMiddleware(gx_wsgi_webapp) gx_app.haltables.append(("WSGI Middleware threadpool", new_wsgi_handler.executor.shutdown)) root_mount.app = new_wsgi_handler # type: ignore[assignment] - GalaxyFileResponse.nginx_x_accel_redirect_base = gx_app.config.nginx_x_accel_redirect_base - GalaxyFileResponse.apache_xsendfile = gx_app.config.apache_xsendfile + _rebind_tus_routes(app, gx_app) + _rebind_galaxy_middleware(app, gx_app) + app.state.route_name_index = build_route_name_index(app) app.openapi_schema = None def _lazy_openapi() -> dict: