axios-auth-refresh
axios-auth-refresh copied to clipboard
onRetry never firing
I'm using axios-auth-refresh with react-query with axios as defaultQueryFn. Everything works fine besides onRetry, it's just never firing (I do not see 1111 in console). My code:
class Api {
constructor() {
this.axios = axios.create({
paramsSerializer: params => qs.stringify(params, {arrayFormat: 'none'}),
});
createAuthRefreshInterceptor(
this.axios,
this.refresh, {
statusCodes: [401],
shouldRefresh: (error) => error?.response?.data?.detail == "Signature has expired",
onRetry: (requestConfig) => console.log(1111) || ({
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: `Bearer ${localStorage.getItem("access_token")}`
}
}),
}
);
}
async refresh(failedRequest) {
try {
const resp = await axios.post(`/api/token/refresh`, {}, {
skipAuthRefresh: true,
headers: {Refresh: localStorage.getItem("refresh_token")}
});
localStorage.setItem("access_token", resp.data.access_token);
localStorage.setItem("refresh_token", resp.data.refresh_token);
failedRequest.response.config.headers['Authorization'] = `Bearer ${resp.data.access_token}`;
//return Promise.resolve();
} catch (err) {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
//return Promise.reject(err);
}
}
}
Example:

The first "me" receives 401 due to expired access token, country is non-protected route, the next 4 requests are suspended by axios-auth-refresh (all those requests are triggered simultaneously by react-query). After the "refresh" request "me" successfully replayed (due to failedRequest.response.config.headers['Authorization'] = 'Bearer ${resp.data.access_token}';), and the next 4 (previously suspended) requests failing again. The last 4 requests are re-triggered by react-query.
I am having the same issue, have you been able to solve this?
I've found a workaround:
axios_inst.interceptors.request.use(request => {
const access_token = localStorage.getItem("access_token");
if (access_token) {
request.headers['Authorization'] = `Bearer ${access_token}`;
}
return request;
});
But for some reason this additional interceptor not working with class property, so axios instance was extracted outside of the class.
From reading the code it seems like it works as expected.
But your mistake is understandable because onRetry is a bit unfortunate name (if what the code does was really an intention).
Because right now onRetry is called only on "stalled" requests, which means requests send after your refreshAuthLogic (refresh token call) gets executed. Requests that were fired before that (right after the first failed request started but before the 401 response for that 1st request was received) are only queued for retry (after refresh finishes) but onRetry is not called for them.
Don't know if this is intended but it seems reasonable to apply onRetry on them too. Probably somewhere in the resendFailedRequest()
TL;DR: there is an issue with the request interceptor being ejected too early and the interceptor is never running properly because the skipAuthRefresh config property is always being set to true
I have also run into the issue of the onRetry function not working.
I am using
"axios": "0.27.2",
"axios-auth-refresh": "3.3.4",
Because right now
onRetryis called only on "stalled" requests, which means requests send after yourrefreshAuthLogic(refresh token call) gets executed. Requests that were fired before that (right after the first failed request started but before the 401 response for that 1st request was received) are only queued for retry (after refresh finishes) butonRetryis not called for them.
@Liwoj if this is the intended behaviour of the library then I do not think it is working. These stalled requests can never go through the library's request interceptor because the interceptor is removed after the token is refreshed
https://github.com/Flyrell/axios-auth-refresh/blob/b2f534588d9123a276b9810bd8b90ebcd647c365/src/index.ts#L65-L68
https://github.com/Flyrell/axios-auth-refresh/blob/b2f534588d9123a276b9810bd8b90ebcd647c365/src/utils.ts#L118-L123
The onRetry function should be called for requests that were captured by the response error interceptor of this package; in the example provided by @Dem0n3D in the original comment of this issue, the onRetry function should have been called for these requests:

I think that there is a bug in the way that the onRetry function is being called. In the response error interceptor
https://github.com/Flyrell/axios-auth-refresh/blob/b2f534588d9123a276b9810bd8b90ebcd647c365/src/index.ts#L46-L70
the resendFailedRequest function is called
https://github.com/Flyrell/axios-auth-refresh/blob/b2f534588d9123a276b9810bd8b90ebcd647c365/src/utils.ts#L142-L145
before the request is being intercepted by the request interceptor of the package
https://github.com/Flyrell/axios-auth-refresh/blob/b2f534588d9123a276b9810bd8b90ebcd647c365/src/utils.ts#L100-L107
There are 2 issues:
- one is caused by lines 143 and 101 of the
utilsfile. TheresendFailedRequestfunction is always setting theskipAuthRefreshconfiguration property totruewhich always causes the request interceptor to skip calling theonRetryfunction. - the other is caused by lines 66 and 68 in the
indexfile. The request interceptor is being ejected by thefinallyfunction on line 66 before it can intercept the request send from line 68.
@Flyrell I think that the test being performed here is not properly testing the code since it is not using the response error interceptor (added using the createAuthRefreshInterceptor function) and is thus skipping the call to the resendFailedRequest method (which is causing the issue). Modifying the test to the following captures the issue:
it('calls the onRetry callback before retrying the request', async () => {
const instance = axios.create();
const onRetry = jest.fn((requestConfig: AxiosRequestConfig) => {
// modify the url to one that will respond with status code 200
return {
...requestConfig,
url: 'https://httpstat.us/200',
};
});
createAuthRefreshInterceptor(instance, (error) => Promise.resolve(), { onRetry });
await instance.get('https://httpstat.us/401');
expect(onRetry).toHaveBeenCalled();
});
The original issue can be tested using the following test:
it('calls the onRetry callback before retrying all the requests', async () => {
const instance = axios.create();
const onRetry = jest.fn((requestConfig: AxiosRequestConfig) => {
// modify the url to one that will respond with status code 200
return {
...requestConfig,
url: 'https://httpstat.us/200',
};
});
createAuthRefreshInterceptor(instance, (error) => Promise.resolve(), { onRetry });
await Promise.all([
instance.get('https://httpstat.us/401'),
instance.get('https://httpstat.us/401'),
instance.get('https://httpstat.us/401'),
instance.get('https://httpstat.us/401'),
]);
expect(onRetry).toHaveBeenCalledTimes(4);
});
Both tests will fail with Request failed with status code 401 because the onRetry function was not called to update the URL to a page that will reply with 200.
Modifying the createRequestQueueInterceptor interceptor to:
cache.requestQueueInterceptorId = instance.interceptors.request.use((request: CustomAxiosRequestConfig) => {
return cache.refreshCall
.catch(() => {
throw new axios.Cancel('Request call failed');
})
.then(() => (options.onRetry ? options.onRetry(request) : request));
});
and the changing the order of the finally and then functions in the response interceptor to:
return refreshing
.then(() => resendFailedRequest(error, getRetryInstance(instance, options)))
.catch((error) => Promise.reject(error))
.finally(() => unsetCache(instance, cache));
seems to fix the issue.
@Flyrell I would be happy to open a PR to fix this issue.
I have created a PR to fix this issue (before I forget what is needed to fix the problem).