Fix CSRF token mismatch Error in Laravel SPA with Axios or Fetch
When you build a Laravel SPA and start sending AJAX requests with Axios or Fetch, you can suddenly get this error: 419 Page Expired CSRF token mismatch. The backend is fine, routes are correct, but Laravel keeps rejecting your request. That happens because Laravel did not receive a valid CSRF token and assumes the request might be forged.
Let’s fix it step by step for both Axios and Fetch.
1. Quick CSRF refresher in Laravel
Laravel protects POST, PUT, PATCH, and DELETE routes with the VerifyCsrfToken middleware. The rule is simple:
- Every state changing request must send a valid token.
- The token can come from:
- A hidden
_tokenfield in forms. - A header like
X-CSRF-TOKENorX-XSRF-TOKEN.
- A hidden
In classic Blade forms Laravel does this automatically. In a SPA you must send the token yourself with JavaScript.
2. Make sure the CSRF token is available to JavaScript
Add this meta tag in your main layout that serves the SPA (usually resources/views/app.blade.php or similar):
<meta name="csrf-token" content="{{ csrf_token() }}">
Now JavaScript can read it:
const csrfToken = document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
We will reuse csrfToken with Axios or Fetch.
3. Fixing CSRF token mismatch with Axios
3.1 Global Axios defaults
Create a file like resources/js/bootstrap.js or any common JS file that runs before your SPA:
import axios from 'axios';
const token = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content');
if (token) {
axios.defaults.headers.common['X-CSRF-TOKEN'] = token;
}
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
If you are using Laravel with cookies (for example Sanctum on same domain), also send credentials:
axios.defaults.withCredentials = true;
Now every Axios request automatically sends the CSRF token.
3.2 Example Axios request
axios.post('/api/profile', {
name: 'John Doe',
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error.response?.data || error.message);
});
As long as the SPA is loaded from the same domain and the middleware is enabled, Laravel should accept this request.
4. Fixing CSRF token mismatch with Fetch API
Fetch does not have global defaults, so you should create a helper function.
const csrfToken = document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content');
async function postJson(url, data = {}) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin', // or 'include' if needed
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
Usage:
postJson('/api/profile', { name: 'John Doe' })
.then(data => console.log(data))
.catch(err => console.error(err));
The important parts are:
X-CSRF-TOKENheader.credentials: 'same-origin'when your SPA is served from the same domain as Laravel.
5. SPA with Laravel Sanctum and XSRF-TOKEN cookie
If you use Laravel Sanctum with cookie based authentication, CSRF works a bit differently:
- Laravel sends a cookie named
XSRF-TOKEN. - Your client must send it back in
X-XSRF-TOKENheader.
5.1 Enable withCredentials or credentials include
Axios:
axios.defaults.withCredentials = true;
Fetch:
credentials: 'include',
5.2 Set the correct header
Axios can do this automatically if you leave xsrfCookieName and xsrfHeaderName default:
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
axios.defaults.withCredentials = true;
For Fetch, read the cookie manually and send it:
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const xsrfToken = getCookie('XSRF-TOKEN');
fetch('/sanctum-protected-route', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': xsrfToken,
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'include',
body: JSON.stringify({ foo: 'bar' }),
});
6. Common reasons CSRF fails in a Laravel SPA
Here are typical problems that cause token mismatch:
- SPA served from a different domain
Example: backend atapi.example.test, front end atlocalhost:5173.
In that case you are in cross site mode. For Sanctum you must configure theSANCTUM_STATEFUL_DOMAINSand CORS correctly. - Missing or outdated meta token
The page was loaded long ago and the session expired. Reload the SPA or handle 419 by refreshing the page and trying again. - Token cached in old HTML
If you use full page cache and forget to invalidate it, the SPA may use a token from a different session. Clear cache:php artisan view:clear php artisan cache:clear - Route excluded or double protected
If you manually added your SPA API routes toapi.php, remember that theapimiddleware group does not include CSRF by default. If you copied routes toweb.phpand also wrote custom CSRF checks, they can conflict. - Session storage not writable
If Laravel cannot store sessions (wrong permissions instorage/framework/sessions), it cannot validate CSRF tokens. Fix permissions and session driver.
7. Handling 419 errors gracefully in a SPA
In production you do not want a blank 419 page. Catch it on the client and guide the user.
Axios example:
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 419) {
alert('Your session expired. Please reload the page.');
window.location.reload();
}
return Promise.reject(error);
}
);
Fetch example:
async function handleResponse(response) {
if (response.status === 419) {
alert('Your session expired. Please reload the page.');
window.location.reload();
return;
}
return response.json();
}
This way, users get a clear message instead of a confusing error.
8. Quick checklist
To fix CSRF token mismatch in a Laravel SPA that uses Axios or Fetch:
- Add
<meta name="csrf-token" content="{{ csrf_token() }}">to your HTML. - Read that token in JavaScript and send it in
X-CSRF-TOKEN. - For Sanctum and cookies, send
X-XSRF-TOKENfrom theXSRF-TOKENcookie and enable credentials. - Make sure SPA and backend domains and CORS settings are correct.
- Check session storage and clear Laravel caches if tokens look stale.
Once the token is sent correctly with every state changing request, the CSRF mismatch error disappears and your Laravel SPA can safely talk to the backend.