Repository navigation
Give each coroutine its own Vite so preload links stay per request - #23
Merged
Merged
Conversation
Vite is a container singleton, so every coroutine in a worker shared one preloaded-assets list. Concurrent requests appended to it, and FlushVite emptied it whenever another request started, so a response's Link header carried another page's preload links or none. RequestScope now hands each request a clone of the boot-time Vite with an empty list. The list is reset directly because Vite::flush() only exists from Laravel 12.10.
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.
Problem
Illuminate\Foundation\Viteis a container singleton on the shared base application, so every coroutine in a worker shared one$preloadedAssetslist. Blade's@viteappends to it,AddLinkHeadersForPreloadedAssetsbuilds theLinkheader from it, andFlushVite(RequestReceived) empties it whenever another request starts. Under concurrency a response'sLinkheader carried another page's preload links, or none.v0.8.34 (#22) made every
Linkvalue reach the client, which exposed this. Production impact (modelslab.com, 2026-10-15 19:45 UTC): 23 of 60 parallel fresh renders had a wrong preload header (7 empty, 12 with other pages' assets, 4 mixed). The app's full-page cache stores that list on a MISS and replays it for 60–300 s.Fix
RequestScoperesolvesIlluminate\Foundation\Viteper coroutine: a clone of the boot-time instance (build directory, nonce, resolvers, prefetch settings are kept) with an empty preload list.@vite(app()), theVitefacade (caching is off in coroutine mode) andFlushViteall go through the scope.The list is reset directly instead of with
Vite::flush(), which only exists from Laravel 12.10 — calling it would throw on Laravel 10/11/12.0–12.9 and, becauseFlushViteresolves Vite on every request, turn every request into a 500.Verification
tests/Unit/RequestScopeViteIsolationTest.php): the clone keeps boot config and starts empty; and the production interleaving — request A renders, request B starts (FlushVite) and renders, A builds its header — read through theVitefacade andapp()with the worker's global wiring. Both tests fail with the fix removed.Vite::flush), 12.39 and 13.36.a0b8a39e0, Octane with the prod flags — 8 workers, 2 task workers,--max-requests=500— against throwaway MySQL/Redis/Mongo; this branch bind-mounted overvendor/.../src):@viteassets).Known limits (unchanged by this PR)
scoped()CSP-nonce generator) is still shared across concurrent requests; a nonce set on one request's Vite clone is not seen by another. Before this PR the nonce lived on the shared Vite and raced instead.Illuminate\Foundation\Viteto its own class, or injects Vite into a boot-time singleton, still gets the shared instance.Need help on this PR? Tag
@codesmith-botwith what you need.