Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.
The problem is, this creates a closure and returns old data to the interceptor when it's being called.
I am using JWT authentication using React/Node and I'm storing access tokens using Context API.
This is how my Interceptor component looks like right now:
import React, { useEffect, useContext } from 'react'; import { Context } from '../../components/Store/Store'; import { useHistory } from 'react-router-dom'; import axios from 'axios'; const ax = axios.create(); const Interceptor = ({ children }) => { const [store, dispatch] = useContext(Context); const history = useHistory(); const getRefreshToken = async () => { try { if (!store.user.token) { dispatch({ type: 'setMain', loading: false, error: false, auth: store.main.auth, brand: store.main.brand, theme: store.main.theme, }); const { data } = await axios.post('/api/auth/refresh_token', { headers: { credentials: 'include', }, }); if (data.user) { dispatch({ type: 'setStore', loading: false, error: false, auth: store.main.auth, brand: store.main.brand, theme: store.main.theme, authenticated: true, token: data.accessToken, id: data.user.id, name: data.user.name, email: data.user.email, photo: data.user.photo, stripeId: data.user.stripeId, country: data.user.country, messages: { items: [], count: data.user.messages, }, notifications: store.user.notifications.items.length !== data.user.notifications ? { ...store.user.notifications, items: [], count: data.user.notifications, hasMore: true, cursor: 0, ceiling: 10, } : { ...store.user.notifications, count: data.user.notifications, }, saved: data.user.saved.reduce(function (object, item) { object[item] = true; return object; }, {}), cart: { items: data.user.cart.reduce(function (object, item) { object[item.artwork] = true; return object; }, {}), count: Object.keys(data.user.cart).length, }, }); } else { dispatch({ type: 'setMain', loading: false, error: false, auth: store.main.auth, brand: store.main.brand, theme: store.main.theme, }); } } } catch (err) { dispatch({ type: 'setMain', loading: false, error: true, auth: store.main.auth, brand: store.main.brand, theme: store.main.theme, }); } }; const interceptTraffic = () => { ax.interceptors.request.use( (request) => { request.headers.Authorization = store.user.token ? `Bearer ${store.user.token}` : ''; return request; }, (error) => { return Promise.reject(error); } ); ax.interceptors.response.use( (response) => { return response; }, async (error) => { console.log(error); if (error.response.status !== 401) { return new Promise((resolve, reject) => { reject(error); }); } if ( error.config.url === '/api/auth/refresh_token' || error.response.message === 'Forbidden' ) { const { data } = await ax.post('/api/auth/logout', { headers: { credentials: 'include', }, }); dispatch({ type: 'resetUser', }); history.push('/login'); return new Promise((resolve, reject) => { reject(error); }); } const { data } = await axios.post(`/api/auth/refresh_token`, { headers: { credentials: 'include', }, }); dispatch({ type: 'updateUser', token: data.accessToken, email: data.user.email, photo: data.user.photo, stripeId: data.user.stripeId, country: data.user.country, messages: { items: [], count: data.user.messages }, notifications: store.user.notifications.items.length !== data.user.notifications ? { ...store.user.notifications, items: [], count: data.user.notifications, hasMore: true, cursor: 0, ceiling: 10, } : { ...store.user.notifications, count: data.user.notifications, }, saved: data.user.saved, cart: { items: {}, count: data.user.cart }, }); const config = error.config; config.headers['Authorization'] = `Bearer ${data.accessToken}`; return new Promise((resolve, reject) => { axios .request(config) .then((response) => { resolve(response); }) .catch((error) => { reject(error); }); }); } ); }; useEffect(() => { getRefreshToken(); if (!store.main.loading) interceptTraffic(); }, []); return store.main.loading ? 'Loading...' : children; } export { ax }; export default Interceptor;
The getRefreshToken
function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.
The interceptTraffic
function is where the issue persists. It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.
You will notice that I am exporting ax
(an instance of Axios
where I added interceptors) but when it's being called outside this component, it references old store data due to closure.
This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.
Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.
Any help is appreciated, thanks.
To pass parameter or argument to Axios interceptor with JavaScript, we can add it to the config. params object. We add the myVar property to config. params by spreading its existing properties into a new object and then add myVar after it.
Axios interceptors are functions that Axios calls for every request. You can use interceptors to transform the request before Axios sends it, or transform the response before Axios returns the response to your code. You can think of interceptors as Axios' equivalent to middleware in Express or Mongoose.
It is a common practice to store the JWT in the localStorage with
localStorage.setItem('token', 'your_jwt_eykdfjkdf...');
on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage
custom-axios.js
import axios from 'axios'; // axios instance for making requests const axiosInstance = axios.create(); // request interceptor for adding token axiosInstance.interceptors.request.use((config) => { // add token to request headers config.headers['Authorization'] = localStorage.getItem('token'); return config; }); export default axiosInstance;
And then, just import the Axios instance we just created and make requests.
import axios from './custom-axios'; axios.get('/url'); axios.post('/url', { message: 'hello' });
If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:
custom-axios.js
import axios from 'axios'; const customAxios = (token) => { // axios instance for making requests const axiosInstance = axios.create(); // request interceptor for adding token axiosInstance.interceptors.request.use((config) => { // add token to request headers config.headers['Authorization'] = token; return config; }); return axiosInstance; }; export default customAxios;
And then import the function we just created, grab the token from state, and make requests:
import axios from './custom-axios'; // logic to get token from state (it may vary from your approach but the idea is same) const token = useSelector(token => token); axios(token).get('/url'); axios(token).post('/url', { message: 'hello' });
I have a template that works in a system with millions of access every day.
This solved my problems with refresh token and reattemp the request without crashing
First I have a "api.js" with axios, configurations, addresses, headers. In this file there are two methods, one with auth and another without. In this same file I configured my interceptor:
import axios from "axios"; import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService"; export const api = axios.create({ baseURL: process.env.REACT_APP_API_URL, headers: { "Content-Type": "application/json", }, }); export const apiSecure = axios.create({ baseURL: process.env.REACT_APP_API_URL, headers: { Authorization: "Bearer " + localStorage.getItem("Token"), "Content-Type": "application/json", }, export default api; apiSecure.interceptors.response.use( function (response) { return response; }, function (error) { const access_token = localStorage.getItem("Token"); if (error.response.status === 401 && access_token) { return ResetTokenAndReattemptRequest(error); } else { console.error(error); } return Promise.reject(error); } );
Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:
import api from "../api"; import axios from "axios"; let isAlreadyFetchingAccessToken = false; let subscribers = []; export async function ResetTokenAndReattemptRequest(error) { try { const { response: errorResponse } = error; const retryOriginalRequest = new Promise((resolve) => { addSubscriber((access_token) => { errorResponse.config.headers.Authorization = "Bearer " + access_token; resolve(axios(errorResponse.config)); }); }); if (!isAlreadyFetchingAccessToken) { isAlreadyFetchingAccessToken = true; await api .post("/Auth/refresh", { Token: localStorage.getItem("RefreshToken"), LoginProvider: "Web", }) .then(function (response) { localStorage.setItem("Token", response.data.accessToken); localStorage.setItem("RefreshToken", response.data.refreshToken); localStorage.setItem("ExpiresAt", response.data.expiresAt); }) .catch(function (error) { return Promise.reject(error); }); isAlreadyFetchingAccessToken = false; onAccessTokenFetched(localStorage.getItem("Token")); } return retryOriginalRequest; } catch (err) { return Promise.reject(err); } } function onAccessTokenFetched(access_token) { subscribers.forEach((callback) => callback(access_token)); subscribers = []; } function addSubscriber(callback) { subscribers.push(callback); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With