javascriptreactjsfetch-api

How can I automatically refresh the token and retry the fetch request after receiving a 401 response?


const acceptRequest = async () => {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_SOCKET_SERVER_URL}/acceptRequest`, {
        method: "POST",
        body: JSON.stringify({ id: enemyid }),
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
        },
      });
      if (res.status === 200) toast("Working");
      if(res.status === 401)await refreshTokenFn()
    } catch (err) {
      if (err instanceof Error) toast(err.message);
      setPending(false);
    }
  };

I have a refreshToken function that I can call to refresh the token. I want the fetch request to retry automatically after refreshing the token, without duplicating the entire code.

This is how I am handling it which is not good enough for me.

const acceptRequest = async () => {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_SOCKET_SERVER_URL}/acceptRequest`, {
        method: "POST",
        body: JSON.stringify({ id: enemyid }),
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
        },
      });
      if (res.status === 200) toast("Working");
      if(res.status === 401){
           await refreshTokenFn()
        const res = await     fetch(`${process.env.NEXT_PUBLIC_SOCKET_SERVER_URL}/acceptRequest`, {
        method: "POST",
        body: JSON.stringify({ id: enemyid }),
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
        },
      });
 if (res.status === 200) toast("Working");
}

    } catch (err) {
      if (err instanceof Error) toast(err.message);
      setPending(false);
    }
  };

Solution

  • Create a wrapper for API requests that can do two things...

    1. Automatically check for 401s and retry requests after refreshing the access token
    2. Queue any requests made while a refresh request is in progress so you're not duplicating refresh requests

    For example

    api.js

    import refreshTokenFn from './somewhere';
    
    // hold any in-progress refresh requests in this promise
    let refreshPromise;
    
    const defaultOptions = { credentials: 'include' };
    
    const api = async (url, options) => {
      // it might be handy to construct full URLs here
      const res = await fetch(
        new URL(url, process.env.NEXT_PUBLIC_SOCKET_SERVER_URL),
        { ...defaultOptions, ...options },
      );
      if (res.ok) {
        return;
      }
      if (res.status === 401) {
        if (!refreshPromise) {
          // note there's no `await`, we just set up the hold on the refresh process
          refreshPromise = refreshTokenFn();      
        }
        await refreshPromise; // wait for in-progress refresh requests
        refreshPromise = null; // clear the promise once resolved
    
        // recursively call the request with the same params
        return api(url, options);
      }
    
      throw new Error(`${res.status} ${res.statusText}`);
    };
    
    export default api;
    

    Then you can just use this function for all API requests without worrying about the refresh logic

    import api from './api';
    
    const acceptRequest = async () => {
      try {
        await api('/acceptRequest', {
          method: 'POST',
          body: JSON.stringify({ id: enemyId }),
          headers: { 'Content-Type': 'application/json' },
        });
        toast('Working');
      } catch (err) {
        toast(err.message);
        setPending(false);
      }
    };
    

    Demo...

    // Mocks
    let authValid = false;
    const fetch = async (url) => {
      console.log('fetch:', url);
      if (!authValid) {
        console.warn('fetch: request unauthorized');
        return { ok: false, status: 401 };
      }
      console.log('fetch: request ok');
      return { ok: true, status: 200 };
    };
    const refreshTokenFn = () => {
      console.log('refreshTokenFn: Refreshing token');
      return new Promise((r) => {
        setTimeout(() => {
          authValid = true;
          console.log('refreshTokenFn: Token refreshed');
          r();
        }, 1000);
      });
    };
    const process = { env: { NEXT_PUBLIC_SOCKET_SERVER_URL: 'https://example.com/' } };
    
    // Answer code
    let refreshPromise;
    
    const defaultOptions = { credentials: 'include' };
    
    const api = async (url, options) => {
      // it might be handy to construct full URLs here
      const res = await fetch(
        new URL(url, process.env.NEXT_PUBLIC_SOCKET_SERVER_URL),
        { ...defaultOptions, ...options },
      );
      if (res.ok) {
        return;
      }
      if (res.status === 401) {
        if (!refreshPromise) {
          // note there's no `await`, we just set up the hold on the refresh process
          refreshPromise = refreshTokenFn();      
        }
        await refreshPromise; // wait for in-progress refresh requests
        refreshPromise = null; // clear the promise once resolved
    
        // recursively call the request with the same params
        return api(url, options);
      }
    
      throw new Error(`${res.status} ${res.statusText}`);
    };
    
    // Test
    ['/acceptRequest', '/some-other-url', '/and-another'].forEach(async (url) => {
      await api(url, {});
      console.log('API request complete:', url);
    });
    .as-console-wrapper { max-height: 100% !important; }