In an Inertia Laravel app, how does the middleware's version() method make a stale browser tab reload after you deploy new frontend assets?
answer
- a hash of build/manifest.json
- X-Inertia-Version on every visit
- only GET visits are compared
- 409 with X-Inertia-Location
- window.location reload, flash kept
basics
~20 sversion() returns a hash of Vite's build manifest; each Inertia visit sends the version it loaded with, and when a GET visit's version differs the server answers 409 with X-Inertia-Location, so the client does a full page load and fetches the new assets.
solid answer
~40 sThe base `Inertia\Middleware::version()` returns an xxh128 hash of `config('app.asset_url')` when set, otherwise of `public/build/manifest.json`, which changes whenever Vite builds new assets. The version travels in the page object, and every Inertia visit sends it back as `X-Inertia-Version`. On a **GET** Inertia request whose header differs from the current version, the middleware's `onVersionChange()` reflashes the session and returns `Inertia::location($request->fullUrl())`: a **409** with `X-Inertia-Location` (and the new version header). The client treats that as a location visit and loads the URL with a full browser navigation, so the new HTML references the new bundles. Since client 3.6, a mismatch on a background request such as polling does not reload; the next user-initiated visit does. Returning `null` from `version()` disables the check.
code
php · 15 lines<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function version(Request $request): ?string
{
// Default: xxh128 of ASSET_URL, else of public/build/manifest.json.
return parent::version($request);
}
}go deeper
Recall that Inertia compares an asset version on navigation and reloads the page when the frontend was redeployed.
Explain the X-Inertia-Version header, the manifest hash from version(), the 409 with X-Inertia-Location and the full browser reload.
Handle the edges: GET-only checks, reflashed sessions, background requests since 3.6, CDN asset URLs, and overrides that break reloading.
Decide how releases and long-lived tabs interact, such as forced reloads versus user prompts, given forms users may lose.
## The problem: an old tab after a deploy An **Inertia** app loads its JavaScript once and then swaps pages over XHR. A visitor who opened the pricing page yesterday still runs yesterday's bundle. After a deploy, the server may send props or component names that old code does not understand, and the old code may request chunk files that no longer exist. Inertia's **asset versioning** detects that mismatch and turns the next navigation into a full page load. ## Where the version comes from The `HandleInertiaRequests` middleware extends `Inertia\Middleware`, whose `version(Request $request)` method returns: 1. `hash('xxh128', config('app.asset_url'))` when `ASSET_URL` is configured (useful when assets live on a CDN path that changes per release); 2. otherwise the xxh128 hash of `public/build/manifest.json`, the manifest Vite writes on every production build; 3. otherwise the hash of `public/mix-manifest.json` for legacy Mix builds; 4. otherwise `null`, meaning versioning is off. You can override `version()` to return any string that changes with the frontend build, or set it with `Inertia::version($value)` or `Inertia::version(fn () => ...)`. ## The round trip 1. On the first load, the page object includes `version`. 2. The client stores it and sends it as the **`X-Inertia-Version`** header on every Inertia visit. 3. The middleware compares the header with `Inertia::getVersion()`, but only for **GET** Inertia requests. 4. On a mismatch, `onVersionChange()` calls `$request->session()->reflash()` so flash messages survive the extra round trip, then returns `Inertia::location($request->fullUrl())`. 5. For an Inertia request, `Inertia::location()` produces an empty **409 Conflict** response with the **`X-Inertia-Location`** header; the middleware also sets `X-Inertia-Version` to the new version. 6. The client sees 409 plus `X-Inertia-Location`, fires its `location` event, and navigates with `window.location` (a plain reload when the URL is the current page). 7. That navigation is a normal full page load, so the new root view links the new, cache-busted Vite files. | Request | Checked? | Outcome on mismatch | |---|---|---| | GET Inertia visit, user clicked a link | yes | 409, full reload to the target URL | | GET background visit (polling, `router.reload`, async) | yes | since client 3.6: no forced reload; the page stays | | POST, PUT, PATCH, DELETE Inertia visit | no | handled normally | | full page load (no `X-Inertia`) | no | already fetches fresh HTML | ## Why a full load fixes it Inertia does not refresh assets itself. It relies on the fact that a full page load renders the Blade root view again, and `@vite` there emits file names containing content hashes from the new manifest. The browser therefore fetches the new bundles, and the old code is gone. ## Background requests since 3.6 A version change detected on a request the user did not initiate, such as a `usePoll` refresh, would reload the page under the user's cursor and could discard unsaved input. Since client **3.6.0**, such async requests do not navigate; the page stays as it is, and the next user-initiated visit hits the same 409 and reloads. You can listen for the `location` event, which reports whether the version changed, to show a "new version available" banner instead. ## Taking manual control Return `null` (or a constant) from `version()` to turn automatic reloads off. You can still share the real hash as a prop, compare it on the client and prompt the user to refresh at a convenient moment. ## Common mistakes - overriding `version()` with a value that never changes, so stale tabs are never reloaded; - returning a value that changes on every request, such as a timestamp, so every GET visit becomes a full reload; - expecting a mismatch on a form POST to reload: only GET visits are compared.
- Why does onVersionChange() reflash the session?The 409 costs an extra request before the real page renders. Without reflashing, a flash message set by the previous request, such as a success notice, would be consumed by the 409 response and missing from the reloaded page.
- What happens if version() returns a timestamp?The version differs on every request, so every GET Inertia visit gets a 409 and turns into a full page load. The app stops behaving like a single-page app.
It is like ordering from last season's printed menu: the waiter notices the date on your menu, says it has changed and hands you the new one before taking the order. The 409 is that remark, and the full page load is receiving the new menu; a guest merely glancing at the table (a background poll) is not interrupted.
saying these in an interview costs you the question
- Inertia polls the server for new asset versions in the background
- A version mismatch returns 404 so the client reloads
- The default version is the Laravel application version string
- Every Inertia request, including POSTs, is version-checked
- Inertia downloads the new bundle itself without a page reload