Laravel error
Laravel: 419 Page Expired
The error
419 | Page ExpiredA 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:clearA 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.
