Generic fetch replay to handle token refresh

Viewed 154

I am looking for a generic way to replay a fetch if I get a 401 response.

My application is a SPA using OIDC. Our frontend developers utilise fetch, and a ServieWorker injects the access_token into the AJAX request before sending the request to the API(s). There are times when a fetch occurs but access_token is expired. When that happens, I want to use the refresh_token to get a new access_token and then replay the fetch, returning the replayed fetch in the Promise. Ideally, this would be something the frontend developers would not even know is happening.

Meaning that a UI developer will code something like what's below (remember, the access_token is injected via ServiceWorker):

fetch("https://backend.api/user/get/1")
.then(resp => 
{
    console.log("user information is XYZ. Raw response:", resp);
})

When really what's happening in the background is:

[Initial request] > [Expired token response] > [Request new token] > [Initial request replayed]

I've experimented with overriding the fetch method with what's below, but I can't figure out a generic way to recreate/clone the original fetch:

window.fetch = new Proxy(window.fetch, {
      apply(fetch, that, args) {
          // Forward function call to the original fetch
          const result = fetch.apply(that, args);

          // Do whatever you want with the resulting Promise
          result.then(resp => 
          {
              if(resp.status == 400 || resp.status == 401)
              {
                let rt = getRefreshToken();

                return fetch("https://idaas.provider/get/new/token", {
                  "method": "POST",
                    "body": new URLSearchParams({
                                grant_type: "refresh_token",
                                refresh_token: rt,
                                client_id: client_id_str
                            })
                    });
                
              }
          }).then(resp =>
          {
            
            let new_token = resp.new_token;
            send_new_token_to_service_worker(new_token);
            return new_token
          }).then(tok =>
          {
            //  How do I replay the original request?
          })

          return result;
      }
});

The goal is to simplify what the UI developers need to handle. I want them focused on UX and have this sort of error handling done in the background.

Note: If necessary, I would be open to not using fetch and instead utilising a wrapper method. Obviously, because of the code already written surrounding fetch, the new method would need to accept the same arguments and produce the same return.

1 Answers
window.fetch = new Proxy(window.fetch, {
  apply(fetch, that, args) {
    let fetchApi = [
      fetch(args.shift())
    ];
    let fetchToken = [
      fetch("https://idaas.provider/get/new/token", {
        method: "POST",
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: getRefreshToken(),
          client_id: client_id_str,
        })
      }).then(token => send_new_token_to_service_worker(token)),
    ];
    return Promise.allSettled(fetchApi).then(results => {
      let rejected = results
        .map(result => result.status)
        .includes("rejected");
      if (rejected) {
        return Promise.all([...fetchToken, ...fetchApi]).then(results => results.at(1));
      } else {
        return results.at(0).value;
      }
    });
  },
});

Usage fetch("https://your-api").then(resp => resp);

Related