Categories
Node.js

How to cancel an HTTP request in Node.js

5 min read

If you’re making an HTTP request in Node.js there’s a good chance you’ll want to cancel it if it takes too long to receive a response. Or perhaps you have a slightly more complex situation where you’re making multiple requests in parallel, and if one request fails you want to cancel all of them. These sound like reasonable things to want to do, but solving these problems is often much less straightforward. You might even end up with a hacky workaround involving setTimeout() (it’s ok, I’ve been there too!).

Fortunately there’s a JavaScript API which gives us a standard way to cancel asynchronous tasks such as an in-flight HTTP request: the Abort API. This small but powerful API has been available in some web browsers since 2017, and now it’s available in Node.js too. Let’s take a look at what the Abort API is, how it works, and how we can use it to cancel an HTTP request in Node.js.

The Abort API

The Abort API is a JavaScript API which consists of two classes: AbortController and AbortSignal. Here’s an example of them in action:

const controller = new AbortController();
const signal = controller.signal;

signal.addEventListener("abort", () => {
  console.log("The abort signal was triggered");
}, { once: true });

controller.abort();

When the controller.abort() method is called, it sets the signal’s aborted property to true. It also dispatches an abort event from the AbortSignal instance in controller.signal to the list of registered handlers. In the example above we’ve registered one handler for the abort event type, which logs a message to let us know that the abort signal was triggered. If you’re using a library or method which accepts an AbortSignal instance as an option, it will attach it’s own event handler for the abort event.

The Abort API originated in the Web Platform. Microsoft Edge 16 was the first browser to implement the Abort API in October 2017, but now all major browsers support it.

Node.js has had Stable support for the Abort API since v15.4.0, released in December 2020. It was backported to Node.js v14.17.0, but with an Experimental status. If you’re using Node.js v14.x or an earlier version of Node.js you’ll probably want to use the abort-controller library (the Node.js core implementation was closely modelled on this library).

You can learn about support for the Abort API in different versions of Node.js in my article ‘Cancel async operations with AbortController‘.

Cancelling an HTTP request with an AbortSignal

The following code examples require Node.js v16.0.0 or greater as they use the promise variant of setTimeout().

Now let’s take what we’ve learnt about the Abort API and use it to cancel an HTTP request after a specific amount of time. We’re going to use the promise variant of setTimeout() as it accepts an AbortSignal instance via a signal option. We’re also going to pass another AbortSignal as an option to a function which makes an HTTP request. We’ll then use Promise.race() to "race" the timeout and the HTTP request against each other.

First, we’ll install a popular implementation of the Fetch API for Node.js, node-fetch:

npm install node-fetch

Then we’re going to import the modules which we’ll be using:

// example-node-fetch.mjs

import fetch from "node-fetch";

import { setTimeout } from "node:timers/promises";

We need to import the promise variant of setTimeout() from the timers/promises module as it’s not a global like the callback setTimeout() function.

Now we’ll create two AbortController instances:

// example-node-fetch.mjs

const cancelRequest = new AbortController();
const cancelTimeout = new AbortController();

Next we’ll create a makeRequest() function which uses node-fetch to make an HTTP request:

// example-node-fetch.mjs

async function makeRequest(url) {
  try {
    const response = await fetch(url, { signal: cancelRequest.signal });
    const responseData = await response.json();

    return responseData;
  } finally {
    cancelTimeout.abort();
  }
}

There are two things to note in the code snippet above:

  • We’re passing cancelRequest.signal (an AbortSignal instance) as a signal option to the fetch() function. This will allow us to abort the HTTP request which fetch() makes.
  • We’re calling the abort() method on our cancelTimeout abort controller when our request has completed. We’ll see why in just a moment.

Now we’ll create a timeout() function which will throw an error after a specified amount of time:

// example-node-fetch.mjs

async function timeout(delay) {
  try {
    await setTimeout(delay, undefined, { signal: cancelTimeout.signal });

    cancelRequest.abort();
  } catch (error) {
    return;
  }

  throw new Error(`Request aborted as it took longer than ${delay}ms`);
}

The promise variant of setTimeout() which we’re using here works quite differently to its better known callback based counterpart. The arguments it accepts are:

  • The number of milliseconds to wait before fulfilling the promise.
  • The value with which to fulfill the promise. In this case it’s undefined as we don’t need a value here – we’re using setTimeout() to delay the execution of the code which comes after it.
  • An options object. We’re passing the signal option here, with the AbortSignal in our cancelRequest abort controller. This allows us to cancel the scheduled timeout by calling the cancelTimeout.abort() method. We do this in the makeRequest() function above after our HTTP request has completed.

If the timeout completes without being aborted, we call the abort() method on our cancelRequest controller. We then throw an error with an error message which explains that the request was aborted.

We’ve wrapped the setTimeout() call in a try / catch block because if the cancelTimeout signal is aborted, the promise returned by setTimeout() rejects with an AbortError. We don’t need to do anything with that error, but we do need to return so that the function doesn’t continue to execute and throw our "request aborted" error.

After defining our makeRequest() and timeout() functions, we pull it all together with Promise.race():

// example-node-fetch.mjs

const url = "https://jsonplaceholder.typicode.com/posts";

const result = await Promise.race([makeRequest(url), timeout(100)]);

console.log({ result });

In the code snippet above we’re using the free fake API provided by JSON Placeholder.

Our call to Promise.race() returns a promise that fulfills or rejects as soon as one of the promises in the array fulfills or rejects, with the value or reason from that promise. With the functions that we’re calling in the array, that means:

  • If the HTTP request made by makeRequest() completes in under 100 milliseconds — as specified by the argument to our timeout() function — the value of result will be the parsed JSON response body. The timeout created by timeout() will also be aborted. If an error occurs when making the HTTP request, the promise returned by Promise.race() will reject with that error.
  • If the HTTP request does not complete in under 100 milliseconds, the timeout() function will abort the HTTP request and throw an error which explains that the request was aborted. Promise.race() will reject with that error.

You can view the full code which we’ve put together in this example on GitHub.

Thanks to James Snell for sharing the Promise.race-based timeout pattern in his article Using AbortSignal in Node.js.

Support for cancelling HTTP requests

Support is growing for the Abort API in Node.js compatible HTTP libraries, and in the Node.js core APIs too.

Libraries

  • Node Fetch
  • Undici
  • Axios. Accepts a signal option since v0.22.0 (released on Oct 1st 2021). You’ll find details on how to pass in AbortSignal in the README, but not yet on the Axios docs website.

Node.js core API methods

The following HTTP related methods in Node.js accept an AbortSignal as a signal option:

Wrapping up

I’m pretty sure we’re going to see a lot more of the Abort API as other libraries, as well as methods in Node.js core, add support for it. Combined with the promise variant of setTimeout, it’s a powerful way to gain more control over your HTTP requests.

I’m looking forward to seeing new patterns emerge around the Abort API. If you’ve got some other ideas of ways it can be used, I’d love to hear about them. Post a comment below or drop me a message on Twitter.

2 replies on “How to cancel an HTTP request in Node.js”

Leave a Reply

Your email address will not be published. Required fields are marked *