TokenMismatchException on an AJAX request almost always means the CSRF token Laravel expects doesn't match the one actually sent — usually because the token was never included in the request at all, or because it's gone stale relative to the current session.
The root cause: Laravel protects every POST/PUT/PATCH/DELETE request by default
The VerifyCsrfToken middleware, active on the web middleware group by default, rejects any state-changing request (anything other than GET/HEAD/OPTIONS) that doesn't include a valid CSRF token matching the current session — this applies to an AJAX request exactly the same way it applies to a normal form submission.
The fix: including the token in every AJAX request via jQuery's ajaxSetup
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
Setting this once via ajaxSetup(), typically in a global JS file loaded on every page, automatically attaches the CSRF token header to every subsequent $.ajax() call made afterward — simpler than manually adding the header to every individual AJAX call throughout the codebase.
Including the token in a raw fetch() call instead
fetch('/products', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
Including it as a hidden form field, for a traditional (non-AJAX) form
The @csrf Blade directive renders a hidden input containing the token — this is the standard approach for a normal form submission, distinct from the header-based approach needed for an AJAX request, since a traditional form has no custom headers to attach a token to.
Why the exception shows up specifically after a long-idle browser tab
The CSRF token is tied to the current session, and a session eventually expires based on its configured lifetime — a browser tab left open for longer than the session lifetime still has the *old* token baked into its already-loaded HTML, so any AJAX request it sends afterward carries a now-stale token that no longer matches the (expired or renewed) session, producing exactly this exception; this is the single most common real-world trigger.
A JavaScript-side fix for the long-idle-tab scenario
$(document).ajaxError(function (event, xhr) {
if (xhr.status === 419) { // Laravel's CSRF token mismatch status code
alert('Your session has expired. The page will now reload.');
window.location.reload();
}
});
Laravel returns HTTP 419 specifically for a CSRF token mismatch — catching this status code globally and prompting a page reload (which fetches a fresh token embedded in the newly loaded HTML) is a reasonable user-facing fallback for the stale-tab scenario, rather than leaving the user looking at a confusing generic error.
Excluding a specific route from CSRF protection entirely
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/*',
]);
})
This should be reserved specifically for routes that genuinely can't include a CSRF token at all — an incoming webhook from a third-party service, which has no access to the site's session or token — never as a general workaround for an AJAX request that could otherwise include the token correctly.