Laravel 419 Error Troubleshooting Quick Reference
Table Of Contents
A quick reference for troubleshooting Laravel's 419 error, with the key things to check.
関連動画
Related Video
This is a video where we actually tried out the content from the article! If anything is unclear in the article, please check out the video.
The video provides further explanations and demonstrations, so it should be helpful.
Subscribe to Our Channel
If you found this video helpful, please consider subscribing to our channel or giving it a thumbs up! It really motivates us to create more content.
Questions and Feedback
If you have any questions or feedback regarding this article or the video, feel free to leave them in the comment section of the video. Your input is greatly appreciated and will help us improve our content in the future!
Laravel 419 Error Troubleshooting Quick Reference
If you see 419 PAGE EXPIRED, start by checking how you're sending the request.
Check out the sections below for more details!
| Symptom or situation | What to check | Fix or troubleshooting approach |
|---|---|---|
| Sending a POST request from a Blade form | Whether the form being submitted contains @csrf and sends _token | Add @csrf inside the form, reopen the page, and submit again |
| Sending a POST request with Ajax, fetch, jQuery, or Axios within Laravel | The meta tag and X-CSRF-TOKEN header | Add the token from the meta tag to the header |
| Using Sanctum + Axios in an SPA | CSRF initialization, the XSRF-TOKEN cookie, and the X-XSRF-TOKEN header | Fetch /sanctum/csrf-cookie before logging in or performing similar actions |
| Hosting the frontend and API on separate subdomains | CORS and Axios settings for sending cookies | Check supports_credentials, withCredentials, and withXSRFToken |
| Hosting the API on a completely different domain | Whether you're confusing this with a subdomain setup | Use API token |
| Investigating the relationship between CSRF and sessions | VerifyCsrfToken and StartSession | See the experiment below, and don't treat disabling protection as a fix |
Getting a 419 when submitting a form even with @csrf added | Whether it's outside the form, or missing from a search form | Placing it inside the form being submitted is a suggested approach |
| Getting a 419 on a page left idle for a long time or when using multiple tabs | Session expiration and logins or logouts in other tabs | Possible steps include reloading the page, logging in again, and checking SESSION_LIFETIME |
| Still getting a 419 after changing settings | Whether cached configuration or other cached data remains | config:clear and cache:clear are possible steps |
| Cookies aren't saved after adding or editing configuration files | Whether a PHP configuration file starts with a blank line | There has been a reported case resolved by moving <?php to the first line |
| The session doesn't persist even though a token is present | Whether StartSession is applied, cookie attributes, and Sanctum settings | Possible steps include checking route middleware and whether cookies are saved and sent |
| Getting a 419 with a load balancer or multiple servers | Whether sessions are shared between servers | A shared store such as Redis is a possible approach |
| Changing the message shown when a 419 occurs | Where exceptions are handled and the Laravel version | Catching TokenMismatchException is one suggested approach |
| CSRF validation fails for a webhook from an external service | Whether the same protection used for browser forms is appropriate | Consider excluding specific routes and verifying the sender's signature |
Solutions
1. I fixed Blade forms by adding @csrf inside the form
When I submitted a form with POST without @csrf, I got this error:
419 PAGE EXPIREDAdding @csrf inside the form being submitted fixed it.
<form method="POST" action="{{ route('post') }}">
@csrf
<input name="post" />
<button type="submit">post</button>
</form>@csrf generates a hidden input tag like this:
<input
type="hidden"
name="_token"
value="vond93ovKGBBXpALpxAu4Ka9V646MW8tm9BvLRFp"
/>To check this, use your browser's developer tools to confirm:
- The HTML contains
name="_token" - The submitted data includes
_tokenin the Network tab
2. I fixed Ajax requests within Laravel by adding the meta tag's token to the header
For asynchronous requests that don't use a form, I handled this by adding the token embedded in a meta tag to the X-CSRF-TOKEN header.
First, I added this meta tag inside the <head> of the page rendered by Blade.
(All the JavaScript, jQuery, and Vue examples below need this code.)
<head>
<!-- Other tags -->
<meta name="csrf-token" content="{{ csrf_token() }}" />
</head>With JavaScript (fetch)
Use document.querySelector to get the token from the meta tag and add it to the fetch headers.
function post() {
const url = "/post"
fetch(url, {
method: "POST",
headers: {
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
"Content-Type": "application/json",
},
body: JSON.stringify({
// Data to send
}),
})
.then(res => {})
.catch(err => console.log(err))
}This code works on its own, even on pages that don't load jQuery.
With jQuery (ajax)
With jQuery's $.ajax, adding the token from the same meta tag does the job too.
let url = "/post"
$.ajax({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
},
url: url,
type: "POST",
data: {
// Data to send
},
})
.done(function () {})
.fail(function (data) {})With Axios in Vue and similar setups
For Axios, I configured the headers in one place in resources/js/bootstrap.js so I wouldn't have to write them for every request.
window.axios = require("axios")
window.axios.defaults.headers.common = {
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
}Whichever approach you use, the thing to check is whether X-CSRF-TOKEN in the outgoing request matches the value in the meta tag.
3. I fixed Sanctum + Axios requests with the cookie approach
If you're using Sanctum + Axios to make requests from a JavaScript framework outside the Laravel project, use this code:
axios.get("/sanctum/csrf-cookie").then(response => {
// Login logic…
})Place it in the entry-point file where the app is initialized, such as resources/js/app.js or main.js, or before createApp().mount() if you're using Vue.
Running separate projects on subdomains
Allowing requests with cookies in config/cors.php does the job.
'supports_credentials' => true,In resources/js/bootstrap.js, add these Axios settings:
axios.defaults.withCredentials = true
axios.defaults.withXSRFToken = trueHere's the Japanese documentation for Laravel 11.x Sanctum.
4. For completely different domains, use API tokens
The approach above worked for my subdomain setup, but when I used a completely different domain for the API server, I couldn't set cookies that way.
So I'll use an API token implementation.
Create an API that sends a token from the backend.
$token = $user->createToken('frontend')->plainTextToken;
return response()->json([
'token' => $token,
]);Store the token received from the API in localStorage or a similar location.
Then add the token to the headers when sending requests with axios, like this:
await axios.get("https://backend-api.com/api/user", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
})Security settings, such as where to store the token and how long to keep it, are tricky with this setup.
Unless you have a specific reason, using the same domain or subdomains is easier.
Common Mistakes
@csrf is outside the form or missing from a search form
When checking, make sure @csrf is inside the form you're actually submitting, not just somewhere on the page.
Session expiration or state changes across multiple tabs
When investigating, record the lifetime value in config/session.php, SESSION_LIFETIME, how long the page was left idle, and the order of logins and logouts in other tabs.
Apparently, possible troubleshooting steps include saving a copy of what you've entered and reopening the page, or logging in again before submitting if needed.
Configuration changes aren't taking effect or cached data remains
Sometimes edits to files such as config/cors.php don't take effect, so try running:
php artisan cache:clear
php artisan config:clearSessions aren't shared in a load-balanced environment
If you have multiple backend servers with a load balancer distributing requests, you need a way to share sessions between the servers.
I use Redis in these situations because it makes session management easier.
To use Redis for sessions in Laravel, adjust these .env settings:
SESSION_DRIVER=redis
SESSION_CONNECTION=default
SESSION_LIFETIME=120
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0Laravel's official docs also seem to recommend choosing a shared store such as Redis or a database when using multiple web servers (official session documentation).
I haven't tried this myself.
How It Works
Middleware defines the relationship between CSRF protection and sessions.
In the setup I was using at the time, CSRF protection was configured in the web middleware group applied to routes/web.php.
As an experiment, I commented out those two settings like this:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];I also removed @csrf from the form.
<form method="POST" action="{{ route('post') }}">
<input name="post" />
<button type="submit">post</button>
</form>Submitting a POST request in this state doesn't produce a 419 error.
That means removing VerifyCsrfToken disabled CSRF protection.
I also found that removing the StartSession middleware stopped CSRF tokens from being generated.
I don't recommend removing these middleware just to get rid of a 419 error.
These notes come from testing a setup for Laravel 10 or earlier, and apparently the configuration lives somewhere else in Laravel 11 and later, but I haven't verified that yet.




