How HTTP Works
4xx · Client errorNon-standard · Laravel
< HTTP/1.1 419 Page Expired

419 Page Expired (Laravel CSRF Token Mismatch)

Laravel 419 Page Expired means the CSRF token did not match the session. Causes: expired session, missing @csrf, dropped cookie, cached forms. Fixes included.

Reviewed 3 min readintermediate5 sourcesTry itMarkdown
Cacheable
No
Retry?
Yes, after reloading the form to get a fresh token
Usually sent by
Laravel app (CSRF middleware)
Spec
Laravel CSRF docs
Often confused with
403, 401
On this page

TL;DR: 419 is Laravel’s own status for a CSRF token mismatch: the token in the request did not match the one in the session. Check that the form has @csrf, that the session did not expire while the page was open, and that the session cookie is actually being stored and sent back.

What it means

419 is not an HTTP standard code. Laravel’s CSRF middleware throws a TokenMismatchException when the token submitted with a POST, PUT, PATCH or DELETE request is missing or does not equal the token in the user’s session, and the framework renders it as 419 with the title “Page Expired”. Browsers show the default page, and AJAX clients that send Accept: application/json get a JSON body:

HTTP/1.1 419 unknown status
Content-Type: application/json

{"message": "CSRF token mismatch."}

The status line often reads unknown status because Laravel’s underlying Symfony response class has no reason phrase for 419. That is cosmetic; the code is what matters.

The token travels as a hidden _token form field, an X-CSRF-TOKEN header, or an X-XSRF-TOKEN header decoded from the XSRF-TOKEN cookie. It is validated by the ValidateCsrfToken middleware (called VerifyCsrfToken in older versions), which is in the web middleware group by default. GET, HEAD and OPTIONS requests are not checked.

Who sent it?

The Laravel application. A reverse proxy or CDN has no reason to produce 419, so if you see it, the request reached PHP. The Set-Cookie: laravel_session=... header on the page that rendered the form (and the XSRF-TOKEN cookie) confirms you are dealing with Laravel’s session layer.

Fix it, in order of likelihood

  1. The form has no token. Every non-GET form needs @csrf (or <input type="hidden" name="_token" value="{{ csrf_token() }}">). For fetch or axios calls, send X-CSRF-TOKEN from a <meta name="csrf-token" content="{{ csrf_token() }}"> tag; Laravel’s default axios setup already sends X-XSRF-TOKEN on same-origin requests.

  2. The session expired while the page was open. The session lifetime defaults to 120 minutes of inactivity. Set it in .env:

    SESSION_LIFETIME=720

    The value is minutes and is read by config/session.php. For long-lived forms, refresh the token with a lightweight keep-alive request or catch the 419 in JavaScript and reload.

  3. The session cookie is not being stored. This shows up as 419 on every login attempt. Check these values:

    SESSION_DOMAIN=.example.com   # must match the host; use null for host-only
    SESSION_SECURE_COOKIE=true    # only when the site is served over HTTPS
    SESSION_SAME_SITE=lax         # "none" requires Secure

    SESSION_SECURE_COOKIE=true on an HTTP-only site means the browser drops the cookie, so each request starts a new session. Behind a TLS-terminating proxy, configure trusted proxies so Laravel sees HTTPS; otherwise it may not set Secure correctly. See Secure and SameSite.

  4. The session is not shared across servers. The file driver writes to storage/framework/sessions on one machine. With several app servers or containers behind a load balancer, use SESSION_DRIVER=redis, database or memcached. Also check the sessions directory is writable.

  5. A cache is serving a stale form. A CDN or full-page cache that stores HTML containing a csrf_token hands every visitor the same token and no matching session. Bypass the cache for pages with forms and for responses that set cookies.

  6. APP_KEY changed or differs between servers. Session cookies are encrypted with it; a mismatch makes every cookie unreadable, so every request is a new session. Run php artisan config:clear after changing it.

  7. A third-party POST has no way to carry a token. Payment webhooks and similar callbacks cannot send one. In Laravel 11 and later:

    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->validateCsrfTokens(except: [
            'stripe/*',
        ]);
    })

    In older apps, add the URI to $except in app/Http/Middleware/VerifyCsrfToken.php. Do not disable CSRF protection globally.

  8. SPA on another domain. With Sanctum, call /sanctum/csrf-cookie first and make sure SANCTUM_STATEFUL_DOMAINS lists the frontend host. A cross-site frontend also runs into SameSite cookie rules.

Reproduce and verify

# Expect 419: no token and no session
curl -i -X POST https://app.example.com/profile -d 'name=test'

# Ask for JSON to see the message
curl -i -X POST https://app.example.com/profile -H 'Accept: application/json' -d 'name=test'

To test the happy path, request the form with -c jar.txt, extract the _token, and post it back with -b jar.txt. If that works but the browser still gets 419, the cookie handling in the browser path is the problem.

403 is what an authorization policy returns, and 401 means authentication is needed. 419 says the request could not be proven to come from your own page. Validation failures in Laravel are 422, not 419.

Frequently asked questions

What does 419 Page Expired mean in Laravel?

Laravel rejected a POST, PUT, PATCH or DELETE because the CSRF token in the request did not match the token stored in the session. Either the token was missing, the session it belonged to expired, or the browser sent a different session than the one that issued the form.

Is 419 an official HTTP status code?

No. It is not in the IANA registry or any RFC. Laravel uses it for a CSRF token mismatch so it can be told apart from 401, 403 and 422, and some HTTP clients and monitoring tools show it as an unknown status.

Why do I get 419 only on production, not locally?

Usually a cookie that is not being stored or returned. Common causes are SESSION_SECURE_COOKIE=true on a site served over plain HTTP, a SESSION_DOMAIN that does not match the host, a file session driver behind several app servers, or a changed APP_KEY. Look at whether the response sets a laravel_session cookie and whether the next request sends it.

How do I exclude a webhook route from CSRF protection?

In Laravel 11 and later, pass the URIs to validateCsrfTokens(except: [...]) in the withMiddleware callback in bootstrap/app.php. In older versions add them to the $except array of the VerifyCsrfToken middleware. Better still, put webhook routes in routes/api.php or outside the web middleware group.

How long before a Laravel form expires?

The token lives as long as the session. SESSION_LIFETIME sets the idle lifetime in minutes in config/session.php, and a form left open longer than that after the last request will return 419 on submit.

Sources

  1. Laravel: CSRF Protectionlaravel.com
  2. Laravel: HTTP Sessionlaravel.com
  3. Laravel Sanctum: SPA Authenticationlaravel.com
  4. IANA HTTP Status Code Registryiana.org
  5. MDN Web Docs: Cross-site request forgery (CSRF)developer.mozilla.org

Keep going

Browse /search