Repository navigation
fix(api): harden core ASGI routes against custom-node mutation - #10211
Conversation
Capture core routes and frontend state before custom-node loading, then serve through a detached guard. Reject unknown API paths and isolate middleware, dependency overrides, and OpenAPI behavior.
fix(api): harden core ASGI routes against custom-node mutation
fix(api): harden core ASGI routes against custom-node mutation
joshistoast
left a comment
There was a problem hiding this comment.
The approach blocks route insertion and mount replacement, but its “immutable core route graph” claim exceeds the protection achieved. The main quality concern is incomplete copying of framework state, compounded by reliance on FastAPI internals. Custom-node API endpoints also intentionally become unavailable—a meaningful compatibility change.
joshistoast
left a comment
There was a problem hiding this comment.
Overall, the revision substantially improves isolation and restores normal routing behavior. This remaining gap still contradicts the documented restriction on custom-node ASGI routes. Heavy reliance on FastAPI internals remains a maintenance cost.
Clone the mounted dispatcher and isolate its static/fallback route configuration. Preserve the live Engine.IO server and its configured path.
Rebuild included WebSocket handlers with the captured dependency override provider.\nTest that guarded routes retain captured overrides after the original app changes.
joshistoast
left a comment
There was a problem hiding this comment.
This predates the latest fix. Current InvokeAI uses a root Socket.IO mount, so no current-production merge blocker or new regression was found.
Retain source routers and routes beside clones during snapshot creation.\nAdd a nested WebSocket regression for reused IDs across prefixes.
Summary
Custom-node code could mutate shared FastAPI route state, register HTTP endpoints, or replace the UI mount before requests reached the application. InvokeAI now captures core route configuration and the original frontend before loading nodes, then serves through a guard outside the mutable router. The production Socket.IO mount is also snapshotted: its wrapper and dispatch configuration are detached while the live Engine.IO server is retained. Core routes, frontend assets, and SPA navigation continue to work; unregistered ASGI routes return 404. Registered invocation classes remain in the OpenAPI schema.
Related Issues / Discussions
None.
QA Instructions
UV_CACHE_DIR=/tmp/uv-cache PYTHONDONTWRITEBYTECODE=1 uv run --active --no-sync pytest tests/app/api/test_trusted_route_guard.py -k trusted_socketio_mount -q -p no:cacheprovider- 1 passed, 20 deselected. Before the fix, the same regression failed because/ws/customreturned 200. The original app serves the injected static file with 200; the guard returns 404.TestClient.__enter__in this environment; a plain FastAPI app reproduces the same AnyIO portal stall.uv run --no-sync pytest -q tests/app/api/test_trusted_route_guard.py -k 'included_websocket_route_keeps_captured_dependency_override or direct_websocket_route_rebinds_dependency_handler or socketio_mount_detaches_dispatcher_configuration'- 3 passed. The regression failed before the fix because the guard called the default dependency; it now verifies that the original app uses a late override while the guard retains the captured override.UV_CACHE_DIR=/tmp/uv-cache uv run --no-sync pytest tests/app/api/test_trusted_route_guard.py -k nested_websocket_routes_keep_their_prefix_clone -q- 1 passed, 20 deselected. Before the fix, this regression failed twice: the nested WebSocket closed without accepting when the clone cache reused another prefix's route.UV_CACHE_DIR=/tmp/uv-cache PYTHONDONTWRITEBYTECODE=1 uv run --active --no-sync pytest tests/app/test_socketio_disconnect.py -q -p no:cacheprovider- 5 passed./invokepolling handshake passed via ASGITransport, with the session on the original Engine.IO server.git diff --check, and commit hooks. Ruff 0.11.2 could not be rerun for follow-ups because the sandbox could not fetch it (R7 DNS failure; R8 tool-cache write blocked).docs/,pnpm test- 29 passed;pnpm build- 736 pages built and all internal links valid (run on the preceding revision; docs have not changed since).Review
The frontend/static, route/middleware/dependency, OpenAPI cache, root-path/lifespan, API fallback, Socket.IO dispatch, and included WebSocket dependency-override findings are resolved. The included WebSocket regression verifies that the original app can accept a later override while the guard keeps the provider captured before snapshot. The clone-cache ID-reuse finding is resolved by retaining each original router and route alongside its clone until snapshot completion. A weakref regression verifies reused IDs cannot misroute nested WebSocket prefixes. This is an ASGI routing boundary, not a Python sandbox: node-pack code still runs in-process and can mutate shared Python objects and services. The snapshot relies on FastAPI 0.141.1 private routing and lifespan internals.
Compatibility / Rollout
Invocation-based node packs continue to use core endpoints, and registered invocation classes appear in OpenAPI. ASGI routes added by node packs during loading or reload are not served; unregistered endpoints return 404. Slash redirects and wrong-method 405 responses remain intact. No persisted-data migration or generated API contract update is required.
Checklist
What's Newcopy (if doing a release after this PR)