Javascript Fetch API too fast; finishes before second HTTP (fetch api) request

Viewed 1276

I have 2 HTTP requests using the Fetch API in Javascript. My main language is Java but I was tasked with a front-end project so I'm not sure if there is a simple solution to this. My problem is that the first call (hits a random server) is hitting an external endpoint that is different than the second external endpoint (this endpoint is on Azure), but the second endpoint is dependent on the first endpoint. Basically, the first endpoint is a POST requests that creates/populates an object/IoT device on the Azure website (IoT hub). So, the second request can't really do anything (in this case PATCH), until the device is showing up on the website. I noticed it usually takes a few seconds, like 1-5 seconds before it appears.

My Fetch API looks something like this:

fetch('https://first-API-endpoint-on-random-server.com/creates-thing-on-Azure', {
  method: 'POST',
  headers: myHeaders,
  body: JSON.stringify(data),
})
.then(response => response.json())
.then(data => {
  console.log('Success:', data);

  // Beginning of nested Fetch
  return fetch('https://second-DIFFERENT-API-endpoint-on-azure.com/tries-to-edit-thing-on-Azure', {
      method: 'PATCH',
      headers: myHeaders,
      body: JSON.stringify(data),
    })
    .then(response => response.json())
    .then(data => {
      console.log('Success:', data);
    })
    .catch((error) => {
      console.error('Error:', error);
    });
})

So basically you click a button on my website UI and its supposed to run this onClick function and create/populate the object/IoT device on Azure by the 1st Fetch API call, then the 2nd Fetch API call is supposed to "PATCH" it or edit the JSON.

The first API call always works but the problem is the second API call seems to always fail for a 404 "device not found" error, because I think the first call is finishing too fast, and then second call tries to PATCH something that doesn't even exist on the website yet. My solution was to divide out/split the API calls on to two different click events, which solves the issue as by the time the user clicks the second button, the 2nd Fetch API call/PATCH usually always succeeds.

I really want to combine them into one Function so the user never knows about the PATCH event/http call. How can I get this second Fetch to actually work?

I can provide any details you wish, please and thank you

1 Answers

It looks like the server's 200 response for the initial request indicates that the request was processed correctly, but not that the resource has been created on Azure. The resource is created on Azure some amount of time following the server's response, but the second request by the client is made some time before the resource has been created. Here is a timeline of a possible sequence of requests and responses:

  1. The client (web browser) makes the 1st (POST) request to the server.
  2. The server receives the request, makes a request to Azure to create a resource, and returns a 200 indicating that the request has been processed. At this point, Azure has received the request and possibly started creating the resource.
  3. The client (web browser) receives the 200 response from the server and proceeds to make the second (PATCH) request. At this point, Azure may be in the process of creating the resource, but has not yet finished creating it, and so it returns a 404.

This problem could be fixed on the backend, for example by having the server wait for the resource to be created on Azure before responding with a 200 to the client. If that is not possible, there are two lines of action that can be taken, separately or in combination, on the client-side:

  • Wait for a fixed amount of time before making the second request, after receiving 200 for the first request. Since we don't have an indication from the backend about when the resource has been created, we can just choose an arbitrary amount of time. Here is an example where we wait for 1 second:
function createResource(data) {
  return fetch('https://first-API-endpoint-on-random-server.com/creates-thing-on-Azure', {
    method: 'POST',
    headers: myHeaders,
    body: JSON.stringify(data),
  })
}

function updateResource(data) {
  return fetch('https://second-DIFFERENT-API-endpoint-on-azure.com/tries-to-edit-thing-on-Azure', {
     method: 'PATCH',
     headers: myHeaders,
     body: JSON.stringify(data),
  })
}

function createAndUpdateResource(data) {
  return new Promise((resolve, reject) => {
    return createResource(data)
      .then(res => res.json())
      .catch(reject)
      .then(resourceData => {
        setTimeout(() => {
          // do we pass data or resourceData data to the PATCH request?
          updateResource(resourceData)
            .then(res => res.json())
            .then(resolve)
            .catch(reject)
          });
        }, 1000);
      });
  });
}

createAndUpdateResource({ name: "abc" })
  .then(res => {
    console.log("success:", res);
  })
  .catch(err => {
    console.log("error: ", err.message);
  });

  • use a retry mechanism for the second request. On getting a 404, we could have some limited number of retries of the same request. This could be combined with the timeout approach described above. Here is an example implementation with retries:
function withRetry(apiCall, retryCount, timeoutMS, statusForRetry) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      apiCall()
        .then(res => {
          if (!res.ok) {
            if (res.status === statusForRetry && retryCount > 0) {
              return withRetry(
                apiCall,
                retryCount - 1,
                timeoutMS,
                statusForRetry
              );
            }
            reject(new Error("couldn't update"));
          }
          return res.json();
        })
        .then(resolve)
        .catch(reject);
    }, timeoutMS);
  });
}

function createAndUpdateResource2(data) {
  return new Promise((resolve, reject) => {
    return createResource(data)
      .then(res => res.json())
      .then(resourceData =>
        withRetry(
          () => updateResource(resourceData),
          3, // 3 retries
          1000, // wait 1 second
          404 // retry on 404
        )
      )
      .then(resolve)
      .catch(reject);
  });
}

Related