Reader Stacks

Handling TokenMismatchException With AJAX Requests in Laravel

This 419 error almost always means the CSRF token sent with an AJAX request is missing, stale, or the session simply expired — the fix depends on which of those three it actually is.

TokenMismatchException, showing up as a 419 status code, means the CSRF token sent with a request doesn't match what the server expects — for an AJAX request specifically, this almost always comes down to one of three causes, each with its own fix.

Cause 1: the CSRF token isn't being sent with the AJAX request at all

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

Standard HTML form submissions include the CSRF token automatically via the @csrf Blade directive — a raw AJAX request needs the token attached manually, via either the X-CSRF-TOKEN header (as above) or as a field in the request body, since Laravel checks both.

Using fetch() instead of jQuery

fetch('/api/orders', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
});

Cause 2: the page has been open long enough that the session (and its token) expired

Laravel's CSRF token is tied to the user's session, and a session has a configured lifetime (SESSION_LIFETIME in .env) — a page left open in a browser tab well beyond that lifetime will have a stale token that no longer matches the server's regenerated session, triggering this exception on the next AJAX call.

A practical fix: refresh the token periodically, or on a specific trigger

setInterval(function () {
    fetch('/csrf-token')
        .then(response => response.json())
        .then(data => {
            document.querySelector('meta[name="csrf-token"]').content = data.token;
        });
}, 1000 * 60 * 30); // refresh every 30 minutes
Route::get('/csrf-token', fn () => response()->json(['token' => csrf_token()]));

For a page that's realistically left open for long stretches (a dashboard, an admin panel), periodically re-fetching a fresh token this way prevents the session-expiry version of this error from happening mid-session.

Cause 3: the route is excluded from CSRF verification when it shouldn't be, or vice versa

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'webhooks/*',
    ]);
})

A genuine webhook endpoint receiving requests from an external service (which can't supply a Laravel session's CSRF token) legitimately needs to be excluded — but excluding a route by accident, or forgetting to exclude one that genuinely needs it, is worth checking specifically when this error appears on a route you didn't expect it on.

Handling the 419 gracefully on the front end, as a fallback

$.ajax({
    url: '/api/orders',
    method: 'POST',
    data: data,
    error: function (xhr) {
        if (xhr.status === 419) {
            alert('Your session has expired. Please refresh the page and try again.');
        }
    }
});

Even with the periodic token refresh in place, handling a 419 gracefully in the AJAX error callback (rather than a generic, unhelpful failure) gives the user a clear, actionable message instead of a confusing silent failure.