Laravel/Vue SPAs: How to send AJAX requests and not run into CSRF token mismatch exceptions

Image for post
Image for post

Creating SPAs or PWAs is very easy in VueJS. As well as creating APIs on Laravel (or Lumen). That’s why I use this pair of Vue + Laravel.

The Problem

And everything is cool until the session is expired and CSRF token is expired too. What to do in this situation? We can’t send API requests anymore because the token is expired. So we can do it either by refreshing a page automatically or asking a user to do so in some messages like “Hey, your session is expired, please click <here> to extend”. Both options are not options actually because we know that the user experience is a pretty important thing so it would be great to not to bother users with randomly refreshing pages (of course there could be some unsaved data) or showing boring messages. The other non-option could be turning off CSRF at all. Which of course is a bad practice and may lead to security pitfalls.

After hours of googling, I found lots of questions and some answers on them but no real solutions which could work for me. Here is my solution to the problem and I hope you will find it useful for your SPA project as well.

The TL;DR; Solution

Image for post
Image for post

We can’t just refresh the current page (the page with expired token), but we can make an additional request to the server to retrieve a page with a new token.

Then we need to parse it and retrieve the token.

And the final step is updating the app state with the new token.

Short story long

Image for post
Image for post

For sending AJAX requests I use axios. I love it for its simplicity and it just works. I’d recommend you to create a single instance of axios and reuse it everywhere. Also avoid using global helpers like axios.get, etc to isolate your requests/responses from other packages. For example, there is a known issue with Laravel Echo which causes all requests (and a whole app) to be down in case of broken connection with server (to be more specific, it causes “Uncaught TypeError: Cannot read property ‘socketId’ of undefined” error which brokes the whole request). Anyhow, using a single instance is pretty easy:

// Http.js
axios from 'axios';
// Automatically add CSRF token to every outgoing request
const baseURL = window.App.base_url;
const headers = {
'X-CSRF-TOKEN' : window.Laravel.csrfToken,
'X-Requested-With': 'XMLHttpRequest',

const Parent = axios.create({
// Let’s omit this so far, see the explanation below
class Http {} extends Parent;
export default Http;

Very simple! Thus in your files you can just import it an use as usual.

// HttpTest.js
import Http from './Http';'/', { abc: 1 })
.then(({ data }) => console.log(data))
.catch(error => console.log(error));

You can set up your interceptors and automatically attach the CSRF token to every outgoing request and track incoming responses by checking whether it’s OK or has some errors:

// Response interceptor
Parent.interceptors.response.use(response => response, error => httpFail(error));

The code above just adds two arrow functions to a response handlers array. So every successful response will be passed through the first arrow function (we just returning what we’ve received without altering it). The main reason for it is to catch and handle failed responses so that we pass it to the httpFail function:

function httpFail(error) {
// Reject on Laravel-driven validation errors
if (error.response && error.response.status === 422) {
return Promise.reject(error);

// Refresh tokens and reject to be further handled be the request initiator
if (error.response && error.response.status === 419) {
return refreshAppTokens().then(() => Promise.reject(error));

// If internal error
if (error.message && !error.response) {
// Due to a possible bug in Laravel Echo, whitelist Echo server error
// See explanation above
if (error.message === "Cannot read property 'socketId' of undefined") {
// showError(error.message);
return Promise.resolve(error);

// Display any other errors to the user and reject
return Promise.reject(error);

// Redirect to log in page if unauthenticated
if (error.response && error.response.status === 401) {
const router = window.router;
const segments = router.currentRoute.path.split('/');
const isAuth = segments.length > 1 && segments[1] === 'auth';

// If not on main page and not on /auth page (change this block or remove accordingly to your app logic)
if (router.currentRoute.path !== '/' && !isAuth) {
return Promise.reject(error);

// Redirect if the backend asks it
if (error.response && error.response.status === 402 && {
return Promise.reject(error);

// Show all other errors
return Promise.reject(error);

If you’re still with me after this chunk of the code, I want to give you a free Web Animations effect as a gift! :)

I hope the code is self-explanatory except refreshAppTokens() function unmentioned before. This is the place where we fetch a new CSRF token:

function refreshAppTokens() {
// Retrieve a new page with a fresh token
.then(({ data }) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = data;
return div.querySelector('meta[name=csrf-token]').getAttribute('content');
.then((token) => {
axios.defaults.headers['X-CSRF-TOKEN'] = token;
window.Laravel.csrfToken = token;
document.querySelector('meta[name=csrf-token]').setAttribute('content', token);

Finally, the last piece of the puzzle is code which sends the request and after it catches 419 error, it tries to re-send the failed request it once again (with a new token, see refreshAppTokens above). Since we want to handle every POST request, let’s modify our Http class as follows:

class Http extends Parent {
static post(url, data, config) {
return new Promise((resolve, reject) => {, data, config)
.then(response1 => resolve(response1))
.catch((error1) => {
console.warn('CSRF token is expired'); // eslint-disable-line no-console
// There is one more try for token mismatch error
if (error1.response && error1.response.status === 419) {, data, config)
.then((response2) => {
const u = new window.URL(decodeURIComponent(location.href));
location.href = `${location.origin}${u.searchParams.get('back')}`;
.catch(error2 => reject(error2));

It just overrides a post static method of axios and hijacks it with some changes. I hope it self-explanatory as well. I believe it might be written in more clear way (there’s always a space for refactoring), but, you got the idea.


With a minimum changes and avoiding to touch Laravel core we’ve achieved the desired result: 1) we kept the CSRF token and didn’t decrease a security layer; 2) we retrieved the new CSRF token without reloading the page and transparently for the user, which is cool in terms of UX. So when the token is expired, the user would not even notice that something just happened and his/her data will be saved!

We just made the Internet a bit more friendly than it was before!

Image for post
Image for post

Owner & CEO @Digital Idea. Professional Full-Stack Web Developer. Lives in: Lutsk, Ukraine. Frameworks: VueJs, Node, Laravel, WordPress, AWS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store