fix: async props callback - #245
Merged
Merged
Conversation
Kumzy
marked this pull request as ready for review
April 28, 2026 14:01
cofin
approved these changes
May 2, 2026
Async prop callbacks and SSR HTTP fetches now resolve on the request event loop in a single async pre-pass via _AsyncInertiaASGIResponse. The to_asgi_response() sync path becomes a fast path for cases with no async work pending; everything async runs on the same task as the route handler, eliminating cross-loop errors with request-scoped resources (asyncpg/aiosqlite/sqlspec sessions). Breaking (Inertia internals): - Removed inertia_plugin.portal and the BlockingPortal lifespan. - Removed portal= from Static/Deferred/Once/Optional/AlwaysProp.render(). - Removed litestar_vite.inertia._async_mixin (AsyncRenderMixin). - Calling render() directly on an unresolved async prop now raises RuntimeError with a clear message. Folded the previous _aresolve.py module into helpers.py, renamed to match existing helpers.py conventions: resolve_async_props (walker) and has_unresolved_async_props (predicate). Each prop class now exposes a resolve_async() method so private state stays in-class. Adds 5 regression tests for nested/multiple/error/once-evaluation/SSR + async cases. Fixes #244.
The Angular E2E examples were failing on `npm install` in CI: pinning @angular/core to 21.2.6 transitively pulled @angular/platform-browser@21.2.5 which has a peerOptional on @angular/animations@21.2.5, conflicting with the root-pinned @angular/animations@21.2.6. Bumping all @angular/* packages to 21.2.10 (where peer-deps are internally consistent — every package at 21.2.x.10 only requires the same 21.2.10), except @angular/build/cli/devkit which max out at 21.2.8 (their caret ranges accept the 21.2.10 runtime packages). Same-batch hygiene bumps for the rest of the example/template ecosystem (vite 8.0.10, react 19.2.5, svelte 5.55.5, vue 3.5.33, hey-api 0.96.1, tailwind 4.2.4, etc.) keeping TypeScript on 5.9.3 and astro on 5.18.1 to avoid major-version risk in this PR. Drive-by fix: replace deprecated `Iterator`/`AsyncIterator` return-type annotations on @contextmanager/@asynccontextmanager-decorated lifespan methods in plugin/__init__.py with `Generator`/`AsyncGenerator` so pyright stops flagging reportDeprecated.
Adds a regression test that closes over a yield-based DI dependency in an optional()/defer() async prop callback. The dependency releases its fake connection on __aexit__ — the test asserts the callback runs *before* that release, i.e. inside Litestar's _call_handler_function AsyncExitStack frame. Currently xfail(strict=True): _AsyncInertiaASGIResponse.__call__ resolves async props during ASGI dispatch, which Litestar runs only after _call_handler_function returns and the DI stack pops. The strict marker forces removal once the handler-fn wrap (litestar-vite-conn-fix.3) lands and the tests start passing. Refs litestar-vite-conn-fix.1, #244
Lift the async-resolution + SSR-prefetch logic out of _AsyncInertiaASGIResponse into a public coroutine on InertiaResponse so the same code path can be driven from inside the route-handler frame (coming in litestar-vite-conn-fix.3). _AsyncInertiaASGIResponse.__call__ now delegates and stays as a fallback until the handler-fn wrap lands. Pure refactor: no behavior change. All 224 inertia tests still pass; the two xfail reproductions still fail with the original traceback. Bundles the existing uv.lock churn from prior dep bumps (commit d9c8337). Refs litestar-vite-conn-fix.2, #244
…prevent unmounting on navigation
cofin
force-pushed
the
fix/inertia-async-props-callback
branch
from
May 2, 2026 20:00
976d00e to
e5276c7
Compare
cofin
added a commit
that referenced
this pull request
May 3, 2026
Six fixes that came out of triaging follow-ups from #245 and a user repro on a remote-host setup. They all touch the response/proxy/bridge layer #245 just rewrote, so they're bundled rather than spread across six PRs. Closes #247. Closes #243. ### Bridge `appUrl` fallback for the dev proxy (#247) Python writes the canonical backend URL into `.litestar.json` (`appUrl: string | null`), derived from `APP_URL` → `LITESTAR_HOST`+`LITESTAR_PORT`/`PORT`. The Vite plugin uses it as the fallback target for the default `/api` and `/schema` proxies when the Vite process doesn't have `APP_URL` exported — common when `npm run dev` runs in a separate shell. User-supplied `server.proxy` and `env.APP_URL` still win. ### SSR prop rendering inside the DI scope Hybrid SSR full-page loads from dict-returning handlers were 500-ing whenever a sync `once()` / `optional()` / `defer()` / shared-props closure captured a yield-based DI dep (asyncpg, SQLAlchemy session, aiosqlite). `_AsyncInertiaSSRResponse.__call__` runs after Litestar's `_call_handler_function` `AsyncExitStack` closes, so the DI resources were already released by the time prop callbacks fired. `InertiaPlugin._wrap_handler_fn` now pre-wraps dict returns into an `InertiaResponse` and resolves async props inside the handler frame — the same path explicit `InertiaResponse` returns already used. SSR prefetch + prop rendering happen while DI is still alive. ### Dev proxy graceful fallback when Vite isn't running `ViteProxyMiddleware` was returning HTTP 503 "Vite server not running" for every asset request when `dev_mode=True` and the Vite hot file was absent — even when a built asset existed on disk. The static files router (registered alongside the proxy) never got a turn. When `_get_target_base_url()` returns `None`, the middleware now passes the request through to the next ASGI app instead of emitting 503. Built assets get served from `bundle_dir`; truly-missing assets return a natural 404. The proxy still wins when the hot file is present. ### Default Vite `server.origin` to bridge `appUrl` for `proxyMode="vite"` `proxyMode="vite"` is supposed to keep all traffic on the Litestar origin, but rendered Jinja templates were emitting absolute Vite URLs (`http://127.0.0.1:5173/...`) that browsers on remote hosts couldn't reach (cloud workstations, devcontainers, dev tunnels, port-mapped Docker). Page rendered, every asset 404'd. When `proxyMode === "vite"`, the user hasn't set `server.origin` explicitly, and the bridge has `appUrl` populated, the JS plugin now defaults `server.origin` to `appUrl`. Vite stamps that origin into the hotfile, the Python loader emits matching absolute URLs, and the proxy gets the requests it was registered to handle. HMR WebSocket terminates at the Litestar origin via the existing route — no user-side WS config needed. The `server.origin: process.env.APP_URL` workaround in `vite.config.ts` is no longer required. ### Mode aliases `ViteConfig.mode` accepted nine values, only five of which were canonical. Three aliases were already normalized at construction (`inertia → hybrid`, `ssr → framework`, `ssg → framework`); `htmx` flowed through unnormalized as a parallel-but-identical synonym for `template`. Added `htmx → template` to `_normalize_mode`. Collapsed `mode in {"template", "htmx"}` membership checks throughout. The mode docstring now enumerates the five canonical modes (`spa`, `template`, `hybrid`, `framework`, `external`) and the four aliases. Also dropped the bogus `template + Inertia` validation rejection — Inertia + Jinja templates is exactly the setup the #243 reporter was trying to use. Only `external` mode (non-Vite dev server like Angular CLI) remains incompatible with Inertia. ### Inertia SSR for `mode="template"` (#243) SSR was silently skipped for `mode="template"` even when `InertiaConfig(ssr=...)` was configured — the SSR endpoint never received a POST and the page rendered entirely client-side. Dropped the `mode == "hybrid"` gate from `_will_render_ssr`; SSR now fires for `mode in {"hybrid", "template"}` whenever `ssr_config` is present. Added `InertiaSSRConfig.target_selector` (default `#app`) — the CSS selector for the element whose outer HTML is replaced by the SSR-rendered body. Hybrid mode continues to use `SPAConfig.app_selector`. `_render_template` now reads `_cached_ssr_payload` and injects the SSR body via `replace_element_outer_html` and any returned head HTML via `inject_head_html`. Same shape as `_render_spa`. The SSR HTTP fetch still happens inside `resolve_async_props` (handler frame), so the DI scope fix above carries through to template mode.
cofin
pushed a commit
that referenced
this pull request
May 3, 2026
#249) ## Summary Fix a 0.22.2 regression where `InertiaPlugin.on_app_init` is silently never called when `VitePlugin` auto-registers it via `ViteConfig.inertia`, breaking the Inertia response envelope, the template `data-page` injection, and the React adapter mount in apps that combine `VitePlugin` with another plugin that rebinds `app_config.plugins` (e.g. older `SQLSpecPlugin`). ## Symptoms After upgrading from 0.22.1 to 0.22.2, apps using `VitePlugin` in Inertia mode silently break: - HTTP responses no longer carry the Inertia JSON envelope (`component` / `props` / `url` / `version`). - The Inertia page payload lands in an orphan `<script id="app_page">` instead of as `data-page="..."` on the root element, so the `@inertiajs/react` adapter cannot find it and React never mounts. - Integration tests asserting the Inertia envelope start failing. ## Root cause `VitePlugin._configure_inertia` constructed an `InertiaPlugin` and appended it to `app_config.plugins`, then relied on Litestar's plugin iterator to call `on_app_init` later. That iterator (in `litestar/app.py`) is built once over a captured reference to `app_config.plugins`, so when a prior plugin does `app_config.plugins = [...]` (rebind), the appended `InertiaPlugin` lands in a list the iterator no longer sees and its `on_app_init` is never called. The same `_configure_inertia` shipped in 0.22.1, but the symptom became visible in 0.22.2 because the response-class swap, middleware install and the new `on_startup` handler-wrap (introduced for async-prop resolution in #245) all run inside `InertiaPlugin.on_app_init`. In 0.22.1 some of that wiring still ran via the `BlockingPortal` lifespan path that 0.22.2 removed, so the broken hand-off was masked. ## Fix 1. `VitePlugin._configure_inertia` now calls `inertia_plugin.on_app_init(app_config)` **eagerly**, instead of waiting for Litestar's iterator. The plugin is still added to `app_config.plugins` so `app.plugins.get(InertiaPlugin)` keeps working (5 callsites in the source depend on it). 2. `InertiaPlugin.on_app_init` is now idempotent via a natural sentinel: `app_config.response_class is InertiaResponse`. `response_class` first call, so the eager invocation plus a subsequent iterator-driven call in healthy plugin orderings is safe (no double middleware / lifespan / on_startup registration).
cofin
added a commit
that referenced
this pull request
May 3, 2026
## Summary Cleans up the Vite plugin's dev proxy and config surface, adds a few more example apps, and bumps the Astro example. ## Changes **Config** - Drops the *"template mode requires Jinja2"* hard validation — HTMX without Jinja and other non-Jinja template engines now work out of the box. - Collapses the mode set to four canonical values (`spa`, `hybrid`, `template`, `framework`) plus 6 capability predicates on `ViteConfig`. `mode='external'` still works but emits a `DeprecationWarning` and forwards to `framework` + `external_dev_server`. - `proxy_mode` is now derived from `mode` instead of being set directly. `proxy_mode='direct'` is removed (raises `ValueError`); `VITE_PROXY_MODE='direct'` is coerced to `vite` with a deprecation warning. Mismatched combinations (e.g. `proxy_mode='proxy'` with `mode='template'`) raise a clear error. **Dev proxy** - The SSR HTTP catch-all moves from a route handler to middleware, eliminating the `Handler already registered for path '/'` collision class. `create_ssr_proxy_controller` is kept as a deprecated alias; WebSocket support moves to a `create_ssr_websocket_handler` factory. - Framework HMR (Astro / Nuxt / SvelteKit) now routes through the Litestar port instead of connecting directly to the framework dev server. The bridge file gains an optional `litestarPort` field; older readers ignore it. **Examples** - Adds `react-router`, `svelte-inertia`, `svelte-inertia-jinja`, `htmx-no-jinja`, `vue-inertia-ssr`, and `vue-inertia-jinja-ssr`. The two SSR examples ship a real Node `/render` runner. - Bumps `examples/astro` from 5.18.1 to 6.2.1. Astro 6 ships rolldown-vite, which doesn't accept `@tailwindcss/vite@4.x`, so the example and the scaffolding template both switch to `@tailwindcss/postcss` (other framework templates are unaffected). **Codegen** - `_extract_schema_ref_name` and `_resolve_component_schema_name` now resolve module-prefixed enum refs to literal unions. Closes #198. ## References - Supersedes #198 - Builds on #245, #248, #249
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix #244