最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Axios Interceptor on request does not wait for await function calls to be finished before proceeding with request -

programmeradmin2浏览0评论

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?

Share Improve this question asked May 3, 2023 at 5:15 RichardRichard 7,4432 gold badges32 silver badges92 bronze badges 0
Add a ment  | 

1 Answer 1

Reset to default 7 +50

There's a few things I'd do differently here...

  1. Always delegate the responsibility of adding the Authorization header to the interceptor. Your consuming code shouldn't need to know about it
  2. Queue requests requiring a token refresh to avoid multiple refresh requests
  3. 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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论