Jonathan Bird Web Development

How to Fix the 419 Page Expired Error in Laravel 12 (2026 Guide)

If you've ever submitted a form in your Laravel application only to be greeted by the dreaded "419 Page Expired" error, you're not alone. This is one of the most common issues Laravel developers encounter, and it's almost always related to CSRF token handling.

In this guide, I'll walk you through what causes the 419 error, how to diagnose it, and the various ways to fix it in Laravel 12 and Laravel 13. Whether you're dealing with expired sessions, missing tokens, or webhook integrations, I've got you covered.

What is the 419 Page Expired Error?

The 419 status code is a non-standard HTTP response that Laravel uses specifically for CSRF (Cross-Site Request Forgery) token mismatches. It's not part of the official IANA HTTP status codes, but Laravel uses it to signal that something went wrong with CSRF verification.

Laravel's CSRF protection exists to prevent malicious websites from making requests on behalf of your authenticated users. Every form submission needs a valid CSRF token that matches the one stored in the user's session. When these don't match, Laravel throws the 419 error.

Common Causes of the 419 Error

Before jumping into fixes, it helps to understand why this error occurs in the first place.

Missing CSRF Token in Forms

The most common cause is simply forgetting to include the CSRF token in your form. Every POST, PUT, PATCH, or DELETE request needs this token.

1{{-- This will trigger a 419 error --}}
2<form method="POST" action="/contact">
3 <input type="text" name="email">
4 <button type="submit">Submit</button>
5</form>
6 
7{{-- This is correct --}}
8<form method="POST" action="/contact">
9 @csrf
10 <input type="text" name="email">
11 <button type="submit">Submit</button>
12</form>

The @csrf Blade directive generates a hidden input field containing the token. You can also use {{ csrf_field() }} instead.

Session Expiration

If a user leaves a page open for too long without activity, their session may expire. When they come back to submit the form, the CSRF token stored in that form no longer matches their new session (or they have no session at all).

This is particularly common on login pages where users might walk away and come back later.

Cookie or Session Configuration Issues

Misconfigured session or cookie settings can prevent Laravel from correctly matching CSRF tokens. This is especially common when:

  • Your SESSION_DOMAIN doesn't match your actual domain
  • You're mixing HTTP and HTTPS protocols
  • The same_site cookie setting is too restrictive
  • You're running behind a load balancer without proper session handling

AJAX Requests Without Token Headers

JavaScript requests need to include the CSRF token in their headers. If you're using Axios with Laravel's default configuration, this is handled automatically. But if you're using fetch or another library, you'll need to handle it manually.

How to Fix the 419 Error

Let's work through the solutions from most common to more advanced scenarios.

Fix 1: Add the @csrf Directive

This is the first thing to check. Make sure every form that uses POST, PUT, PATCH, or DELETE includes the @csrf directive:

1<form method="POST" action="{{ route('contact.store') }}">
2 @csrf
3 
4 <label for="name">Name</label>
5 <input type="text" id="name" name="name" required>
6 
7 <label for="email">Email</label>
8 <input type="email" id="email" name="email" required>
9 
10 <label for="message">Message</label>
11 <textarea id="message" name="message" required></textarea>
12 
13 <button type="submit">Send Message</button>
14</form>

For PUT, PATCH, or DELETE requests using method spoofing, you'll also need the @method directive:

1<form method="POST" action="{{ route('posts.update', $post) }}">
2 @csrf
3 @method('PUT')
4 
5 {{-- form fields --}}
6</form>

Fix 2: Configure CSRF Token for AJAX Requests

For JavaScript-based requests, you need to send the CSRF token in the request headers. First, add a meta tag to your layout:

1<head>
2 <meta name="csrf-token" content="{{ csrf_token() }}">
3</head>

Then configure your JavaScript to include this token. Here's how to do it with different approaches:

Using Axios (Laravel's default):

Axios is pre-configured in Laravel to read the XSRF-TOKEN cookie automatically. If you're using Laravel's default setup, it should work out of the box. But if you need to configure it manually:

1import axios from 'axios';
2 
3axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
4 
5// Now your requests will include the token
6axios.post('/api/contact', {
7 name: 'John Doe',
9});

Using Fetch:

1const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
2 
3fetch('/api/contact', {
4 method: 'POST',
5 headers: {
6 'Content-Type': 'application/json',
7 'X-CSRF-TOKEN': csrfToken,
8 },
9 body: JSON.stringify({
10 name: 'John Doe',
11 email: '[email protected]'
12 })
13});

Using jQuery:

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

Fix 3: Check Your Session Configuration

Open your config/session.php file (or check your .env file) and verify these settings:

1// config/session.php
2 
3return [
4 'driver' => env('SESSION_DRIVER', 'file'),
5 
6 // Session lifetime in minutes
7 'lifetime' => env('SESSION_LIFETIME', 120),
8 
9 // Expire session on browser close
10 'expire_on_close' => false,
11 
12 // Cookie settings
13 'cookie' => env('SESSION_COOKIE', 'laravel_session'),
14 'path' => '/',
15 'domain' => env('SESSION_DOMAIN'),
16 'secure' => env('SESSION_SECURE_COOKIE'),
17 'http_only' => true,
18 'same_site' => 'lax',
19];

Key things to check:

SESSION_DOMAIN: If your application runs on app.example.com, your SESSION_DOMAIN should be .example.com (with the leading dot for subdomain support) or app.example.com.

SESSION_SECURE_COOKIE: If you're using HTTPS (and you should be), set this to true. If it's true but you're accessing via HTTP, cookies won't be sent.

same_site: The lax setting is recommended. Using strict can cause issues with users coming from external links, although please think about your specific circumstances, and err on the side of strict. Using none requires secure to be true.

Your .env file might look like:

1SESSION_DRIVER=file
2SESSION_LIFETIME=120
3SESSION_DOMAIN=.yourdomain.com
4SESSION_SECURE_COOKIE=true

Fix 4: Exclude Routes from CSRF Protection

Sometimes you need to accept requests from external services that can't provide a CSRF token. Payment webhooks from Stripe, Paddle, or other services are a perfect example.

In Laravel 11, 12 and 13, you exclude routes in your bootstrap/app.php file:

1// bootstrap/app.php
2 
3use Illuminate\Foundation\Application;
4use Illuminate\Foundation\Configuration\Middleware;
5 
6return Application::configure(basePath: dirname(__DIR__))
7 ->withRouting(
8 web: __DIR__.'/../routes/web.php',
9 commands: __DIR__.'/../routes/console.php',
10 )
11 ->withMiddleware(function (Middleware $middleware): void {
12 $middleware->validateCsrfTokens(except: [
13 'stripe/*',
14 'webhooks/*',
15 'api/external/*',
16 ]);
17 })
18 ->create();

Important: Only exclude routes that genuinely need to accept external requests. Never exclude routes like login or user settings pages, as this exposes your users to CSRF attacks.

If you're still on Laravel 10 or earlier, you'd add exceptions to the $except property in app/Http/Middleware/VerifyCsrfToken.php:

1// app/Http/Middleware/VerifyCsrfToken.php (Laravel 10 and earlier)
2 
3namespace App\Http\Middleware;
4 
5use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
6 
7class VerifyCsrfToken extends Middleware
8{
9 protected $except = [
10 'stripe/*',
11 'webhooks/*',
12 ];
13}

Fix 5: Create a Custom 419 Error Page

Sometimes sessions will expire, and there's not much you can do about it. What you can do is provide a helpful error page that tells users what to do.

Create a file at resources/views/errors/419.blade.php:

1@extends('layouts.app')
2 
3@section('content')
4<div class="container mx-auto px-4 py-16 text-center">
5 <h1 class="text-4xl font-bold mb-4">Page Expired</h1>
6 
7 <p class="text-gray-600 mb-8">
8 Your session has expired. This usually happens if you've been away for a while
9 or have multiple tabs open.
10 </p>
11 
12 <div class="space-x-4">
13 <a href="{{ url()->previous() }}"
14 onclick="event.preventDefault(); window.location.reload();"
15 class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700">
16 Refresh Page
17 </a>
18 
19 <a href="{{ route('home') }}"
20 class="bg-gray-200 text-gray-800 px-6 py-3 rounded-lg hover:bg-gray-300">
21 Go Home
22 </a>
23 </div>
24</div>
25@endsection

Fix 6: Implement Token Refresh for Long Forms

If your application has forms that users spend a long time on (think: writing blog posts, completing surveys), you can refresh the CSRF token in the background to prevent expiration.

Add this JavaScript to pages with long forms:

1// Refresh CSRF token every 10 minutes
2setInterval(async () => {
3 try {
4 const response = await fetch('/csrf-token');
5 const data = await response.json();
6 
7 // Update meta tag
8 document.querySelector('meta[name="csrf-token"]').setAttribute('content', data.token);
9 
10 // Update all hidden _token inputs
11 document.querySelectorAll('input[name="_token"]').forEach(input => {
12 input.value = data.token;
13 });
14 
15 console.log('CSRF token refreshed');
16 } catch (error) {
17 console.error('Failed to refresh CSRF token:', error);
18 }
19}, 10 * 60 * 1000); // 10 minutes

Create a simple route to return a fresh token:

1// routes/web.php
2 
3Route::get('/csrf-token', function () {
4 return response()->json(['token' => csrf_token()]);
5});

Debugging CSRF Issues

If you're still seeing 419 errors after trying these fixes, here are some debugging steps.

Check if the Token Exists

Add this to your form to verify the token is being generated:

1<form method="POST" action="/test">
2 @csrf
3 <p>Token: {{ csrf_token() }}</p>
4 <button type="submit">Submit</button>
5</form>

Verify Session is Working

Check that sessions are functioning correctly:

1// In a test route
2Route::get('/debug-session', function () {
3 session(['test' => 'value']);
4 
5 return [
6 'session_id' => session()->getId(),
7 'test_value' => session('test'),
8 'csrf_token' => csrf_token(),
9 ];
10});

Check Browser Developer Tools

Open your browser's developer tools and check:

  1. Network tab: Is the _token field being sent with the request?
  2. Application/Storage tab: Are cookies being set correctly? Look for laravel_session and XSRF-TOKEN.
  3. Console: Any JavaScript errors that might be preventing form submission?

Clear Everything

Sometimes cached configurations cause issues. Clear all caches:

1php artisan config:clear
2php artisan cache:clear
3php artisan route:clear
4php artisan view:clear

Also try clearing your browser cookies for your development domain.

Special Cases

Laravel Sanctum SPA Authentication

If you're building an SPA that authenticates via Laravel Sanctum, you need to request a CSRF cookie before making authenticated requests:

1// First, get the CSRF cookie
2await fetch('/sanctum/csrf-cookie', {
3 credentials: 'include',
4});
5 
6// Now you can make authenticated requests
7await fetch('/api/user', {
8 credentials: 'include',
9});

The credentials: 'include' is crucial for sending cookies with cross-origin requests.

Load Balanced Environments

If your application runs behind a load balancer, ensure all instances share the same session storage. Using the database, redis, or memcached session drivers is recommended:

1SESSION_DRIVER=redis
2REDIS_HOST=your-redis-server

Also ensure your load balancer forwards the necessary headers and consider using sticky sessions if session sharing isn't possible.

Testing Without CSRF

When running tests, Laravel automatically disables CSRF protection. If you need to explicitly test with CSRF enabled, use the withMiddleware method:

1public function test_form_submission_requires_csrf(): void
2{
3 $response = $this->withMiddleware()
4 ->post('/contact', [
5 'email' => '[email protected]',
6 ]);
7 
8 $response->assertStatus(419);
9}

Summary

The 419 Page Expired error in Laravel is almost always a CSRF token issue. Here's a quick checklist:

  1. Check your forms have the @csrf directive
  2. Configure AJAX requests to include the CSRF token header
  3. Review session configuration especially SESSION_DOMAIN and SESSION_SECURE_COOKIE
  4. Exclude webhook routes that need to accept external requests
  5. Create a friendly 419 error page for better user experience
  6. Implement token refresh for long-running forms

CSRF protection is an important security feature that you shouldn't disable globally. Take the time to configure it properly and your users will be protected from cross-site request forgery attacks.


Having trouble with CSRF tokens or other Laravel issues? I specialise in Laravel development and debugging complex application issues. Get in touch to discuss your project.

Topics

Syntax highlighting by Torchlight

More articles

Laravel CSV Import Validation: Preventing XSS with League CSV

When building applications that accept CSV file uploads, there's a critical security concern that's easy to overlook. The data inside those files can be just as dangerous as any user input.

Read article

Migrating from Algolia to Typesense: Laravel & Vue InstantSearch Guide

Algolia's 2025 pricing increases prompted me to migrate a B2B e-commerce platform with 15K products to Typesense resulting in ~65% cost savings with similar performance. This guide covers the complete Laravel Scout and Vue InstantSearch migration process, including schema configuration, the InstantSearch adapter setup, filter syntax differences, and a script to convert Algolia synonyms to Typesense format.

Read article

Talk to me about your website project