On the cancellation of repeated HTTP requests

Time:2021-11-28

Scenario:

As developers, we mostly contact crud and joint debugging of various interfaces, but we seem to pay less attention to the HTTP request we send. When the request is not fully completed and a new request is sent, how should we deal with the current request? We know that in order to prevent repeated actions, we can use anti shake and throttling to avoid them, but today we talk about how to avoid repeated requests from the request level, not from the user side. In fact, there are many attempts by developers in this field on the Internet, but some are not so clear. Therefore, after investigating some knowledge in this field, combined with the scenes they usually encounter in development. Summarize the two most common scenarios where HTTP requests need to be cancelled.

Scenario 1:

The same request needs to be cancelled. The same request here refers to the same method, parameters and URL for get requests, and the same method, body and URL for post requests

Scenario 2:

The routing address has changed (the previous web page request has no meaning)

Implementation method:

First of all, to cancel a duplicate request, we need to do two steps. The first step is to know how to cancel it, and the second step is how to judge that the current request is a duplicate request

Implementation method for canceling duplicate requests:

In this part, I will explain how to cancel from two aspects: Axios and fetch, because the cancellation methods of the two request methods are different

In order to facilitate understanding, we use react to demonstrate this process more directly, because we know that the hook function useeffect. The characteristics of useeffect determine that the return function will perform a cleaning operation before each useeffect execution. Therefore, we put the cancellation operation here to simulate canceling the previous operation every time

axios

First of all, let’s introduce Axios. The front-end users must be sick of contact. In a word, the essence of the cancellation of Axios is to use its internally encapsulated canceltoken.

First of all, you should know that the token determines the uniqueness, which is also the identification to determine which request needs to be cancelled. It can be generated by canceltoken. Source()

Source contains the cancel method, which we call to cancel

useEffect(() => {
  const cancelToken = axios.CancelToken;
  const source = cancelToken.source();
  setAxiosRes("axios request created");
  getReq(source).then((res) => {
    setAxiosRes(res);
  });
  return () => {
    source.cancel("axios request cancelled");
  };
}, [axiosClick]);
export const instance = axios.create({
  baseURL: "http://localhost:4001",
});
export const getReq = async (source) => {
  try {
    const {
      data
    } = await instance.get("/", {
      cancelToken: source.token,
    });
    return data;
  } catch (err) {
    if (axios.isCancel(err)) {
      return "axios request cancelled";
    }
    return err;
  }
};

It should be noted here that the cancel action itself can be captured by the catch part and is also an err. We use the iscancel method provided by it to judge whether it is a cancellation operation, which can be used to verify whether our cancellation is successful

fetch:

The situation of fetch is different. The implementation method of fetch is to cancel through signal. Its internal abortcontroller () can cancel all requests responding to the signal mark. It can also simulate once with react. In fact, the essence is the same, and it can also be caught

export const instance = axios.create({
      useEffect(() => {
        const controller = new AbortController();
        const signal = controller.signal;
        setFetchRes("fetch request created");
        hitApi(signal).then((res) => {
          setFetchRes(res);
        });
        //cleanup function
        return () => {
          controller.abort();
        };
      }, [fetchClick]);

The hitapi function is as follows, that is, put the signal into our fetch so that we can abort.

 export const hitApi = async (signal) => {
        try {
          const response = await fetch("http://localhost:4001/", {
            signal
          });
          const data = await response.json();
          return data;
        } catch (err) {
          if (err.name === "AbortError") {
            return "Request Aborted ";
          }
          return err;
        }
      }

Here, similarly, ‘aborterror’ can be captured in catch

Determine whether to repeat

Well, the cancellation function is implemented. Next, we need to consider how to judge whether the request is repeated. There is no doubt that to judge whether there is a duplicate, you want to find whether there is a duplicate request with constant time complexity. Of course, you use map, so that you can find the duplicate request at the speed of O (1), so that you can decide to cancel it. Moreover, it is conceivable that the whole process needs to add something to the array, and those that have been cancelled need to be taken out, so we need an add function and a remove function

const addPending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pending.has(url)) { // If the current request does not exist in pending, add it
      pending.set(url, cancel)
    }
  })
}

When assigning a value to config.canceltoken, you should pay attention to whether the current config.canceltoken already has a value

To facilitate the tiling of parameters, we can use QS to convert object to string

const removePending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  if (pending.has(url)) { // If the current request identity exists in pending, you need to cancel the current request and remove it
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}

Axios interceptor

However, in actual projects, we usually have an Axios interceptor to uniformly manage our requests, so many people like to directly add these two methods to the Axios interceptor, which will be done once and for all.

axios.interceptors.request.use(config => {
  removePending(options) // Check previous requests to cancel before the request starts
  addPending(options) // Add current request to pending
  // other code before request
  return config
}, error => {
  return Promise.reject(error)
})
axios.interceptors.response.use(response => {
  removePending(response) // Remove this request at the end of the request
  return response
}, error => {
  if (axios.isCancel(error)) {
    console.log('repeated request: ' + error.message)
  } else {
    // handle error code
  }
  return Promise.reject(error)
})

Route switching usage

Finally, let’s talk about the second scenario. When the route is switched, it’s relatively simple. Just clear our pending queue and connect directly:

export const clearPending = () => {
  for (const [url, cancel] of pending) {
    cancel(url)
  }
  pending.clear()
}
router.beforeEach((to, from, next) => {
  clearPending()
  // ...
  next()
})

Effect demonstration:

On the cancellation of repeated HTTP requests

As you can see, if you click repeatedly, the request is cancelled, and if it returns successfully, it is 200.

Principle analysis:

function CancelToken(executor) { 
 if (typeof executor !== 'function') { 
 throw new TypeError('executor must be a function.'); 
 } 
 var resolvePromise; 
 this.promise = new Promise(function promiseExecutor(resolve) { 
 resolvePromise = resolve; // Expose the inside 
 }); 
 var token = this; 
 //Executor (cancel method); 
 executor(function cancel(message) { 
 if (token.reason) { 
 // Cancellation has already been requested 
 return; 
 } 
 //Token.reason is an instance of cancel 
 token.reason = new Cancel(message); 
 resolvePromise(token.reason);// Change promise status 
 }); 
 }

In essence, the core of canceltoken is to mount the promise, and then do not take the initiative to resolve or reject, but first expose the initiative, that is, the resolvepromise in the code, and then change the state of the promise in the cancellation function.

What is the use of changing this state? It needs to be understood in combination with the xhradapter source code. Here we can see where abort is. The red part is the process of changing the promise state through the above, which is executed in. Then.

function xhrAdapter(config) { 
 return new Promise(function dispatchXhrRequest(resolve, reject) { 
 if (config.cancelToken) { 
 //In the request, listen for the promise state change in canceltoken 
 config.cancelToken.promise.then(function onCanceled(cancel) {
 if (!request) {
 return;
 }
 request.abort();
 reject(cancel); 
 request = null; 
 }); 
 } 
 }) 
}

Conclusion:

In fact, HTTP request cancellation is not a very strange thing. To realize similar request cancellation, there are many other methods, and even many methods are better than this implementation method. For example, canceling the current request instead of canceling the previous one seems more logical, but this article focuses on exposing the idea of resolve, which is worth learning from.

Text / Lily

Pay attention to the technology of getting things and be the most fashionable technical person!