I have this interceptor code for all requests made using Axios. instance
here is the Axios instance.
instance.interceptors.request.use(async (config) => {
const tokens = JSON.parse(localStorage.getItem('users'));
if (!tokens) {
return config;
}
const accessTokenValid = Date.now() <= (tokens?.expirationTime ?? Date.now());
if (accessTokenValid) {
return config;
}
const refreshToken = tokens.refreshToken;
if (!refreshToken) {
return config;
}
const response = await fetch(`${some-endpoint}/auth/refresh/token?token=${refreshToken}`);
const newTokens = (await response.json()).data.data;
localStorage.setItem('users', JSON.stringify(
{
accessToken: newTokens.access_token,
expirationTime: Date.now() + parseInt(JSON.parse(newTokens.expires_in)) * 1000 - 600 * 1000,
refreshToken: newTokens.refresh_token,
}
));
config.headers.Authorization = `Bearer ${newTokens.access_token}`;
return config;
}, (error) => {
console.error(error);
});
Then, the instance
will be used like this.
return instance.get(
"/users/" + id,
options
);
where options
look like this.
{
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
The problem with the code above is that the first request after the token expires will always fail. Then, the value in localStorage.users
will be updated and the second request will succeed because the value has been replaced with the new access token.
I don't want to use an interceptor in response to retry the original request. I want to handle the token refresh on request. After investigating it turns out that the sequence of HTTP requests happening is as follows.
- HTTP request to the endpoint
users
- Then, HTTP request to the endpoint for the token refresh
This tells me that the async/await
part in the request interceptor isn't working. How can I make it so that HTTP requests must wait for the interceptor to finish running first (including awaiting any async functions) before running the HTTP requests itself?
I have this interceptor code for all requests made using Axios. instance
here is the Axios instance.
instance.interceptors.request.use(async (config) => {
const tokens = JSON.parse(localStorage.getItem('users'));
if (!tokens) {
return config;
}
const accessTokenValid = Date.now() <= (tokens?.expirationTime ?? Date.now());
if (accessTokenValid) {
return config;
}
const refreshToken = tokens.refreshToken;
if (!refreshToken) {
return config;
}
const response = await fetch(`${some-endpoint}/auth/refresh/token?token=${refreshToken}`);
const newTokens = (await response.json()).data.data;
localStorage.setItem('users', JSON.stringify(
{
accessToken: newTokens.access_token,
expirationTime: Date.now() + parseInt(JSON.parse(newTokens.expires_in)) * 1000 - 600 * 1000,
refreshToken: newTokens.refresh_token,
}
));
config.headers.Authorization = `Bearer ${newTokens.access_token}`;
return config;
}, (error) => {
console.error(error);
});
Then, the instance
will be used like this.
return instance.get(
"/users/" + id,
options
);
where options
look like this.
{
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
The problem with the code above is that the first request after the token expires will always fail. Then, the value in localStorage.users
will be updated and the second request will succeed because the value has been replaced with the new access token.
I don't want to use an interceptor in response to retry the original request. I want to handle the token refresh on request. After investigating it turns out that the sequence of HTTP requests happening is as follows.
- HTTP request to the endpoint
users
- Then, HTTP request to the endpoint for the token refresh
This tells me that the async/await
part in the request interceptor isn't working. How can I make it so that HTTP requests must wait for the interceptor to finish running first (including awaiting any async functions) before running the HTTP requests itself?
1 Answer
Reset to default 7 +50There's a few things I'd do differently here...
- Always delegate the responsibility of adding the
Authorization
header to the interceptor. Your consuming code shouldn't need to know about it - Queue requests requiring a token refresh to avoid multiple refresh requests
- Use Fetch or Axios for all requests. Mixing them just leads to a larger blast radius and more difficult testing
// Create instance
const instance = axios.create({ baseURL: "https://echo.zuplo.io/" });
let refreshTokenPromise; // used to queue up requests
// Add interceptor
instance.interceptors.request.use(async (config) => {
const tokens = JSON.parse(localStorage.getItem('users'));
if (!tokens) {
console.warn("No tokens");
return config;
}
const accessTokenValid = Date.now() <= (tokens.expirationTime ?? Date.now());
if (accessTokenValid) {
console.log("Token still valid");
// Now add the header
config.headers.Authorization = `Bearer ${tokens.accessToken}`;
return config;
}
const refreshToken = tokens.refreshToken;
if (!refreshToken) {
console.warn("No refresh token");
return config;
}
// Check for in-flight refresh requests and start one if required
refreshTokenPromise ??= getRefreshedTokens(refreshToken);
const newTokens = await refreshTokenPromise;
refreshTokenPromise = null; // clean in-flight state
localStorage.setItem('users', JSON.stringify(newTokens));
config.headers.Authorization = `Bearer ${newTokens.access_token}`;
return config;
});
// Auth requests
const authApi = axios.create(/* baseURL, headers, etc */);
const getRefreshedTokens = async (token) => {
console.log("Refresh auth token");
// return (await authApi.get("/auth/refresh/token", { params: { token } })).data.data.data;
// mock data instead
return delay({
access_token: "new_access_token",
expires_in: new Date(Date.now() + 3600000), // +1 hour
refresh_token: "new_refresh_token",
});
};
// Mocks (localStorage doesn't work in Snippets)
const localStorage = {s:{},setItem(k,v){this.s[k]=v},getItem(k){return this.s[k]??null}};
const delay = (v,d=2000)=>new Promise(r=>setTimeout(r,d,v));
// Demo setup
localStorage.setItem("users", JSON.stringify({
accessToken: "old_access_token",
expirationTime: new Date(2020, 0, 1), // very expired
refreshToken: "refresh_token",
}));
// Simulate simultaneous requests
[123, 456, 789].forEach(async (id) => {
console.log("Getting user", id);
const {
data: {
url,
headers: {
authorization
}
}
} = await instance.get(`/users/${id}`);
console.log("user response:", { url, authorization });
});
.as-console-wrapper { max-height: 100% !important; }
<script src="https://cdnjs.cloudflare./ajax/libs/axios/1.4.0/axios.min.js"></script>
You can see that the sequence resolves the refresh token first and only once before making the user requests with the updated access token.