The problem: A form submission or redirect suddenly throws All Inertia requests must receive a valid Inertia response, even though the route worked fine before.
Environment: Laravel 11, Inertia.js 1.x, Vue 3 (also applies to React adapter), PHP 8.3
The Error
Error: All Inertia requests must receive a valid Inertia response, however a
plain JSON response was received.
at handleFinishedRequest (inertia.js:412)
at handleResponse (inertia.js:389)
Why It Happens
Inertia expects every response from the server — including redirects, validation errors, and error pages — to come back as an Inertia response wrapped with the special X-Inertia header. When any of these things happen, Laravel breaks that contract:
1. A controller method returns response()->json(...) directly instead of Inertia::render(...) — common when a route was copy-pasted from an API controller.
2. A middleware, exception handler, or third-party package (auth packages, rate limiters) intercepts the request and returns a plain JSON error response — for example a 401 or 403 from a package that doesn't know Inertia exists.
3. You hit an unhandled exception in local/dev mode, and Laravel's default exception page renders full HTML instead of routing through Inertia's error page handling.
Inertia's frontend JS is strict on purpose — it needs the X-Inertia header and a JSON page-object shape to know how to swap components without a full page reload. Any deviation throws this exact error.
The Fix
First, check the controller action that's failing and make sure it renders through Inertia, not raw JSON:
- return response()->json(['project' => $project]);
+ return Inertia::render('Projects/Show', [
+ 'project' => $project,
+ ]);
If the request is legitimately correct but a middleware is the culprit (auth guards, rate limiters returning JSON), tell Laravel's exception handler to convert those responses into Inertia-friendly redirects. Add this in your exception handler:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->respond(function (Response $response, Throwable $e, Request $request) {
if ($request->header('X-Inertia') && in_array($response->getStatusCode(), [401, 403, 404, 419, 429, 500, 503])) {
return back()->with('error', $e->getMessage());
}
return $response;
});
})
For local dev exceptions specifically, make sure Ignition/Laravel's debug page isn't rendering on Inertia-tagged requests — that HTML response is the #1 cause during active development.
Step-by-Step
- Find the exact route/controller method that's failing using the Network tab — check its response headers for a missing
X-Inertiaheader. - If it's your own controller, swap any
response()->json()call forInertia::render(). - If it's middleware or a package, add the
respond()handler shown above inbootstrap/app.php. - Clear route and config cache:
php artisan route:clear && php artisan config:clear. - Reproduce the original action and confirm the response now includes the
X-Inertia: trueheader in DevTools. - Check validation-heavy forms specifically — a custom
FormRequestwith a manualfailedValidation()override is a frequent hidden cause.
Edge case: If you're using Laravel Sanctum for API + Inertia in the same app, Sanctum's stateful API guard can return a plain JSON 401 for expired tokens on routes it thinks are API calls, even though the frontend is an Inertia page. Double-check your stateful domains config in config/sanctum.php — a missing domain entry silently breaks Inertia's response contract only on that route.
Hitting a stubborn Inertia response error you can't isolate? Message me on the Contact page and I'll help debug it.