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 @csrf10 <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_DOMAINdoesn't match your actual domain - You're mixing HTTP and HTTPS protocols
- The
same_sitecookie 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 @csrf3 @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 token6axios.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',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 close10 'expire_on_close' => false,11 12 // Cookie settings13 '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=file2SESSION_LIFETIME=1203SESSION_DOMAIN=.yourdomain.com4SESSION_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 Page17 </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 Home22 </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 inputs11 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.php2 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 @csrf3 <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:
- Network tab: Is the
_tokenfield being sent with the request? - Application/Storage tab: Are cookies being set correctly? Look for
laravel_sessionandXSRF-TOKEN. - Console: Any JavaScript errors that might be preventing form submission?
Clear Everything
Sometimes cached configurations cause issues. Clear all caches:
1php artisan config:clear2php artisan cache:clear3php artisan route:clear4php 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 cookie2await fetch('/sanctum/csrf-cookie', {3 credentials: 'include',4});5 6// Now you can make authenticated requests7await 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=redis2REDIS_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(): void2{3 $response = $this->withMiddleware()4 ->post('/contact', [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:
- Check your forms have the
@csrfdirective - Configure AJAX requests to include the CSRF token header
- Review session configuration especially
SESSION_DOMAINandSESSION_SECURE_COOKIE - Exclude webhook routes that need to accept external requests
- Create a friendly 419 error page for better user experience
- 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 articleMigrating 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