Skip to content

Laravel error

Laravel: 419 Page Expired

The error

419 | Page Expired

A 419 is not a bug in your form. It is the CSRF middleware reporting that the session it expected to find was not there, and the reason is almost never the token itself.

The error

419 | PAGE EXPIRED

Nothing else. No stack trace, no line number, and in production not even a hint about which check failed - which is why this one is guessed at more than almost any other error in the framework.

What it means

VerifyCsrfToken compared the token in the request against the token in the session, and they did not match. Laravel converts the resulting TokenMismatchException into an HTTP 419.

Read that carefully, because the wording matters: the comparison failed. That happens when the submitted token is wrong, and it happens just as often when there was no session to compare against at all. The second case is the common one, and it is why so much time gets spent staring at a form that is perfectly correct.

Work through it in this order

Is the token in the form? The cheapest thing to rule out. A hand-written <form> needs @csrf inside it; a fetch or axios call needs the token in a header. If the page is server-rendered Blade, view the source and confirm a hidden _token field is actually there.

<form method="POST" action="/orders">
    @csrf
    ...
</form>

Is a session being written at all? Submit the form, then look for the session cookie in the browser's storage panel and for a session record on the server - a file under storage/framework/sessions, a row in the sessions table, a key in Redis. If nothing is being written, the token has nothing to be compared with and every request will 419 regardless of what the form contains.

Does the cookie come back? A SESSION_DOMAIN that does not match the host being served, a SESSION_SECURE_COOKIE set true on a plain HTTP request, or a SameSite policy fighting a cross-site POST all produce the same symptom: the cookie is set, the browser declines to return it, and the next request arrives with no session.

Is more than one machine answering? The file session driver keeps sessions on local disk. Put two servers behind a load balancer and half the requests land on the machine that has never seen this visitor. Redis, a database table or any shared store fixes it; sticky sessions hide it.

Is the configuration you are reading the configuration that is running?

php artisan config:clear

A cached config file predating your last .env change is not an exotic failure. It is Friday afternoon on most teams.

What looks like a fix and is not

Putting the route in $except. It removes the check rather than satisfying it, and the check is the only thing standing between your form and a page on another domain that posts to it on a logged-in visitor's behalf. A $except entry is correct for a webhook you authenticate another way. It is never correct for a form your own users submit.

Extending SESSION_LIFETIME to a week. That is not a fix either, but it is worth knowing the difference: it genuinely does reduce 419s for people who leave tabs open, at the cost of sessions that stay valid far longer than most security reviews would accept. If long-lived forms are the actual problem, the better answer is to detect the expiry in the page and say so.

Make it stop being mysterious

The default error page tells the visitor nothing and tells you less. Give the 419 a page of its own that says the session expired and offers a way back, and log the mismatches with the route and the session id so a spike is visible before somebody reports it.

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (TokenMismatchException $e, Request $request) {
        Log::warning('csrf mismatch', [
            'route' => $request->path(),
            'has_session' => $request->hasSession(),
        ]);
 
        return redirect()->back()->withInput()
            ->withErrors(['session' => 'Your session expired. Please try again.']);
    });
})

has_session in that log line is the whole diagnosis. If it is false, stop looking at the form.

A 419 on every request across the whole application, right after a deploy, is usually a changed or missing APP_KEY. The session cookie is encrypted with it. And the redirect the middleware attempts afterwards needs somewhere to go, so an unnamed login route turns the 419 into a different exception.

The cached-config and multi-server cases are both deploy problems, and four steps keep a deploy from producing them. If the real question is who is allowed to submit this form at all, the check you want is authorisation.

Related questions

Should I add the route to the CSRF exception list?
Only if it is genuinely not a browser form - an incoming webhook, a callback from a payment provider, an endpoint authenticated by a signature you verify yourself. Adding your own login or checkout route to that list does stop the 419, in the same sense that removing a smoke alarm stops the noise.
Why does it work on my machine and fail in production?
Because the two differ in the things this error is actually about: the session driver, whether more than one server answers requests, the cookie domain, and whether HTTPS is terminated somewhere in front of the application. A single local server with a file-backed session hides every one of those.
It happens only after the page has been open a while.
That is the session lifetime doing what it is configured to do. The token belongs to the session, so when the session expires the token stops matching. The fix is not a longer lifetime - it is telling the visitor, before they have typed six paragraphs into a textarea, that they need to sign in again.
Can a 419 be a sign of an attack?
In principle it is exactly what the middleware exists to report. In practice, a spike of them on one route is far more likely to be a misconfiguration you have just deployed, and a slow trickle is bots posting to your login form. Look at the ratio before reaching for a security explanation.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page