How to Fix the 419 Page Expired Error in Laravel 13 (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 that new 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. Everything here applies to Laravel 12 and below, but I've updated it for Laravel 13 and its new origin-aware CSRF protection. Whether you're dealing with expired sessions, missing tokens, or webhook integrations, I've got you covered in this article.
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.
In Laravel 13, the CSRF middleware was renamed from ValidateCsrfToken to PreventRequestForgery and gained a new origin-aware verification layer. The middleware now checks the browser's Sec-Fetch-Site header first, and only falls back to token validation if origin verification can't be performed. This means same-origin requests in modern browsers may pass without a token check at all, but you'll still see 419 errors when the fallback kicks in and tokens don't match.
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 13, you exclude routes using the new preventRequestForgery method in your bootstrap/app.php file:
1// bootstrap/app.php (Laravel 13) 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->preventRequestForgery(except: [13 'stripe/*',14 'webhooks/*',15 'api/external/*',16 ]);17 })18 ->create();
If you're on Laravel 11 or 12, the method is called validateCsrfTokens instead:
1// bootstrap/app.php (Laravel 11 & 12)2 3->withMiddleware(function (Middleware $middleware): void {4 $middleware->validateCsrfTokens(except: [5 'stripe/*',6 'webhooks/*',7 'api/external/*',8 ]);9})
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});
Fix 7: Use Laravel 13's Origin-Only Mode
If you're on Laravel 13 and your application is served over HTTPS, you can opt into origin-only mode. This skips token validation entirely and relies solely on the Sec-Fetch-Site header that modern browsers send automatically. The upside is that you won't see 419 errors caused by expired tokens or missing @csrf directives at all.
1// bootstrap/app.php (Laravel 13 only)2 3->withMiddleware(function (Middleware $middleware): void {4 $middleware->preventRequestForgery(originOnly: true);5})
A couple of things to be aware of with this approach:
- Failed requests return a 403 response instead of 419, since there's no token mismatch involved.
- The
Sec-Fetch-Siteheader is only sent over HTTPS, so this won't work on plain HTTP (like local development without SSL). Make sure your local environment uses HTTPS if you enable this. - Older browsers that don't send
Sec-Fetch-Sitewill have their requests rejected outright, since there's no token fallback.
If your application needs to accept requests from subdomains (for example, dashboard.example.com posting to api.example.com), you can allow same-site requests in addition to same-origin:
1->withMiddleware(function (Middleware $middleware): void {2 $middleware->preventRequestForgery(allowSameSite: true);3})
For most applications, the default two-layer approach (origin check first, token fallback second) is the safest option. But if you're running a modern HTTPS-only setup, origin-only mode can eliminate an entire class of token-related 419 headaches.
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
- Consider origin-only mode if you're on Laravel 13 with HTTPS
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
How to Fix "Target Class Does Not Exist" Error in Laravel 13 (2026 Guide)
The "Target class does not exist" error in Laravel is usually caused by incorrect controller references, missing imports, or autoloading issues. This guide covers all the common causes and how to fix them in Laravel 13.
Read articleHow to Fix "Failed to Open Stream: Permission Denied" Error in Laravel 13 (2026 Guide)
The "failed to open stream: Permission denied" error in Laravel is almost always a file ownership or permissions issue with the storage and bootstrap/cache directories. This guide covers all the common causes and how to fix them in Laravel 13.
Read article