javascriptjwtes6-promisefetch-apiasynchronous-javascript

order of execution of several fetch calls


What I'm trying to do is: from my single page application, get data from an API using fetch. If the JWT is expired, ask the API for a new JWT and then renew the request for data.

I tried to implement this with 3 functions: getData calls fetchData. If fetchData returns a JWT expired message, a function getjwt is called (also from within getdata) and then (only then) fetchData again.

The application calls getData (upon page load - but that's irrelevant here)

I have a function getjwt:

async function getjwt() {
// posts a refresh token and gets a jwt in headers
let adresse = `${baseUrl}/auth/getjwt`
let headers = { 'Content-Type': 'application/json' }

postData(adresse, refresh.value, headers)
.then((response) => {
  if (response.status === 200) {
    jwt.value = response.headers.get('__auth__')  
  }
})
.catch((err) => alert(err))
}

and the functions fetchData and getData:

async function fetchData (url = '', headers = {}) {
  let r = await fetch(url, {
    method: 'GET', 
    mode: 'cors', 
    cache: 'no-cache', 
    credentials: 'include', 
    headers: headers,
    redirect: 'follow', 
    referrerPolicy: 'no-referrer', 
  })
  return r
}

async function getData(url = '', headers = {}) {
  let r = await fetchData(url, headers)
  if (r.status === 409) {
    console.log("Oh no! My token has expired.")
    getjwt()
    let r = await fetchData(url, headers)
    return r.json()
  } 
  return r.json()
}

On purpose, first time fetchData is called, the jwt is expired. getjwt is called and the jwt get properly renewed (in a Pinia store). BUT: the order of execution is: getData gets called twice, and then getjwt is called.

Maybe I'm missing something about asynchroneous operations and Promise, but how can I force getjwt to be called and finish getting the new jwt before getData is called a second time.


Solution

  • Problem with the getJwt function is that the promise returned by it doesn't wait for the asynchronous operation inside it to finish. As a result, the promise it returns gets resolved before postData gets completed.

    You can fix this by returning the promise from postData as shown below:

    async function getjwt() {
      ...
    
      return postData(...)
        .then((response) => {
          ...
        })
        .catch((err) => {
          ...
          throw err;
        });
    }
    

    Adding a return before the postData call fixes the issue because return ensures that the promise returned by the getJwt function gets resolved to the promise returned by return postData(...). One promise p1 getting resolved to another promise p2 simply means that the promise p1 will wait for p2 to settle (fulfil or reject). If p2 fulfils, p1 fulfils with the same value. If p2 gets rejected, p1 also gets rejected with the same rejection reason.

    Note the change in the catch method. The throw err is needed to avoid implicitly converting the promise rejection into promise fulfilment.

    As you aren't using the await keyword inside the function, you might as well just remove the async keyword from the function signature.

    And finally, to make it work, you need to await the call to the getJwt function inside the getData function.

    async function getData(url = '', headers = {}) {
      ...
      if (r.status === 409) {
        ...
        await getjwt()
        ...
      } 
      ...
    }
    

    I would refactor your code by making the following two improvements:

    async function getjwt() {
      // posts a refresh token and gets a jwt in headers
      let adresse = `${baseUrl}/auth/getjwt`;
      let headers = { 'Content-Type': 'application/json' };
    
      const response = await postData(adresse, refresh.value, headers);
    
      if (response.status === 200) {
        jwt.value = response.headers.get('__auth__');
      }
    }