Add Retries to HTTP requests
Tags: node, http, webdev, beginners
Published: 2020-05-12 14:18:26

_📣 This post originally appeared as [Add Retries to Your API Calls](https://blog.bearer.sh/add-retry-to-api-calls-javascript-node/) on [The Bearer Blog](https://blog.bearer.sh)._ 
<!--kg-card-begin: markdown-->

Whether you're making API calls from Node.js or in the browser, connection failures are going to happen eventually. Some request errors are valid. Maybe the endpoint was wrong or the client sent the wrong data. Other times you can be sure that the error is the result of a problem with the connection to the server or one of the many hops in-between. While API and web service monitoring can inform you about the problem, a more active solution can take care of it for you.

To fix this, you can improve your HTTP request library by adding intelligent retry functionality. This kind of remediation is crucial to ensuring your API calls are successful. Some libraries, like [got](https://www.npmjs.com/package/got) support retrying failed requests out of the box, while others like [axios](https://www.npmjs.com/package/axios) require a separate plugin. If your preferred library doesn't support them, this article is for you. We'll explore adding status-code-specific retries to a request, making them incrementally longer with a technique called "back-off", and more.

## The basics of a retry

To decide when to retry a request, we need to consider what to look for. There are a handful of [HTTP status codes](https://blog.bearer.sh/common-http-status-codes/) that you can check against. This will let your retry logic differentiate between a failed request that is appropriate to retry—like a gateway error—and one that isn't—like a 404. For our examples, we will use 408, 500, 502, 503, 504, 522, and 524. You could also check for [429](https://blog.bearer.sh/error-429-too-many-requests-what-to-do-when-youve-been-rate-limited/), as long as you incorporate the `Retry-After` header into the back-off logic.

The next consideration we want is how often to retry. We will start with a delay, then increase it each additional time. This is a concept known as "back-off". The time between requests will grow with each attempt. Finally, we'll also need to decide how many attempts to make before giving up.

Here's an example of the logic we'll be using in pseudo-code:

1.  If total attempts > attempts, continue
2.  if status code type matches, continue
3.  if (now - delay) > last attempt, try request
4.  else, return to the start

We could also check for things like error codes (from Node.js), and limit retrying to certain methods. For example, ignoring POST is often a good idea, to ensure no duplicate entries are created.

## Recursive request structure

To make this all work, we'll be making a request from within a failed request. This requires the use of recursion. Recursion is when a function calls itself.

![recursion example](https://blog.bearer.sh/content/images/2020/03/node-retry-2.png)

For example, if we wanted to infinitely keep trying to make a request it might look like this:

```javascript
function myRequest(url, options = {}) {
  return requests(url, options, response => {
    if (response.ok) {
      return response
    } else {
      return myRequest(url, options)
    }
  })
}
```

Notice that the `else` block returns the `myRequest` function. Since most modern HTTP request implementations are promise-based, we can return the result. This means that to the end user, the whole process looks like a normal call. For example:

```javascript
myRequest("https://example.com").then(console.log(response))
```

With a plan in mind, let's look at implementing retries in javascript.

## Add retry to Fetch

First, we'll start with the browser's [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The fetch implementation will be similar to the recursion example above. Let's implement that same example, but using fetch and a status check.

```javascript
function fetchRetry(url, options) {
  // Return a fetch request
  return fetch(url, options).then(res => {
    // check if successful. If so, return the response transformed to json
    if (res.ok) return res.json()
    // else, return a call to fetchRetry
    return fetchRetry(url, options)
  })
}
```

This will work to infinitely retry failed requests. _Note: a `return` will break out of the current block, so we don't need an else statement after `return res.json()`_.

Now let's add in a max number of retries.

```javascript
function fetchRetry(url, options = {}, retries = 3) {
  return fetch(url, options)
    .then(res => {
      if (res.ok) return res.json()

      if (retries > 0) {
        return fetchRetry(url, options, retries - 1)
      } else {
        throw new Error(res)
      }
    })
    .catch(console.error)
}
```

The code is mostly the same, except we've added a new argument and a new condition. Add the `retries` argument to the function, with a default value of `3`. Then, rather than automatically calling the function on failure, check if any retries are remaining. If so, call `fetchRetry`. The new `retries` value passed to the next attempt is the current retries minus 1. This ensures that our "loop" decrements, and eventually will stop. Without this, it would run infinitely until the request succeeds. Finally, if `retries` is not greater than zero, throw a new error for `.catch` to handle.

To give it a try, you can make a request to `https://status-codes.glitch.me/status/400`. For example:

```javascript
fetchRetry("https://status-codes.glitch.me/status/400")
  .then(console.log)
  .catch(console.error)
```

If you check your network traffic, you should see _four_ calls in total. The original, plus three retries. Next, let's add in a check for the status codes we want to retry.

```javascript
function fetchRetry(url, options = {}, retries = 3) {
  const retryCodes = [408, 500, 502, 503, 504, 522, 524]
  return fetch(url, options)
    .then(res => {
      if (res.ok) return res.json()

      if (retries > 0 && retryCodes.includes(res.status)) {
        return fetchRetry(url, options, retries - 1)
      } else {
        throw new Error(res)
      }
    })
    .catch(console.error)
}
```

First, declare an array of status codes we want to check for. You could also add this as part of the configuration, especially if you implemented this as a class with a more formal configuration. Next, the retry condition checks to see if the response's `status` exists in the array using [ECMAScript's `array.includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes). If it does, try the request. If not, throw an error.

There is one last feature to add. The incremental back-off delay between each request. Let's implement it.

```javascript
function fetchRetry(url, options = {}, retries = 3, backoff = 300) {
  /* 1 */
  const retryCodes = [408, 500, 502, 503, 504, 522, 524]
  return fetch(url, options)
    .then(res => {
      if (res.ok) return res.json()

      if (retries > 0 && retryCodes.includes(res.status)) {
        setTimeout(() => {
          /* 2 */
          return fetchRetry(url, options, retries - 1, backoff * 2) /* 3 */
        }, backoff) /* 2 */
      } else {
        throw new Error(res)
      }
    })
    .catch(console.error)
}
```

To handle the "wait" mechanic before retrying the request, you can use `setTimeout`. First, we add our new configuration argument (1). Then, set up the `setTimeout` and use the `backoff` value as the delay. Finally, when the retry occurs we also pass in the back-off with a modifier. In this case, `backoff * 2`. This means each new retry will wait twice as long as the previous.

Now if we try out the function by calling `fetchRetry('https://status-codes.glitch.me/status/500')`, the code will make the first request immediately, the first retry after waiting 300ms, the next 600ms after the first response, and the final attempt 900ms after the second response. You can give it a try with any status code by using `https://status-codes.glitch.me/status/${STATUS_CODE}`.

## Further configuration and better options

This is a great solution for one-off requests or small apps, but for larger implementations, it could be improved. Building a configurable class (or class-like object) will give you more control and allow for separate settings for each API integration. You could also apply this logic to a [circuit breaker](https://blog.bearer.sh/build-a-circuit-breaker-in-node-js/) or any other remediation pattern.

Another option is to use a tool that observes and reacts to anomalies in your API calls. At [Bearer](https://bearer.sh), our team is building just that. Instead of configuring all of this in code for each API, the Bearer Agent handles it all for you. [Give it a try](https://www.bearer.sh/) today and let us know what you think [@BearerSH](https://twitter.com/bearersh)

## Bonus: Add retry to Node's native `http` module

The fetch implementation above works for the browser, but what about Node.js? You could use a fetch-equivalent library like [node-fetch](https://www.npmjs.com/package/node-fetch). To make things interesting, let's look at applying the same concepts above to Node.js' native `http` module.

To make things a bit easier, we'll use the shorthand `http.get` method. The retry logic will stay the same, so check out our article on [making API calls with http.request](https://blog.bearer.sh/node-http-request/) if you want to perform requests other than `GET`.

Before we get started, we'll need to change `http.get` from event-based to promise-based so we can interact with it the same way we did with fetch. If you are new to promises, they are an underlying concept that modern async implementations use. Every time you use `.then` or _async/await_, you are using promises under the hood. For the purposes of this article, all you need to know is that a promise can `resolve` or `reject`—in other words, the code passes or fails. Let's look at some code without any retry logic.

Here's a basic `GET` using `http.get`

```javascript
let https = require("https")

https.get(url, res => {
  let data = ""
  let { statusCode } = res

  if (statusCode < 200 || statusCode > 299) {
    throw new Error(res)
  } else {
    res.on("data", d => {
      data += d
    })
    res.end("end", () => {
      console.log(data)
    })
  }
})
```

To summarize, it requests a url. If the `statusCode` isn't in a defined "success range" (Fetch has the `ok` property to handle this) it throws an error. Otherwise, it builds a response and logs to the console. Let's look at what this looks like "promisified". To make it easier to follow, we'll leave off some of the additional error handling.

```javascript
function retryGet(url) {
  return new Promise((resolve, reject) => {
    https.get(url, res => {
      let data = ""
      const { statusCode } = res
      if (statusCode < 200 || statusCode > 299) {
        reject(Error(res))
      } else {
        res.on("data", d => {
          data += d
        })
        res.on("end", () => {
          resolve(data)
        })
      }
    })
  })
}
```

The key parts here are:

1.  Returning a new `Promise`
2.  `resolve` on successful actions
3.  `reject` on errors

We can then test it by calling `retryGet("https://status-codes.glitch.me/status/500").then(console.log).catch(console.error)`. Anything outside the 200 range will show up in our `catch`, while anything within the range will show up in `then`.

Next, let's bring all the logic from the fetch example into `retryGet`.

```javascript
function retryGet(url, retries = 3, backoff = 300) {
  /*  1 */
  const retryCodes = [408, 500, 502, 503, 504, 522, 524] /* 2 */
  return new Promise((resolve, reject) => {
    https.get(url, res => {
      let data = ""
      const { statusCode } = res
      if (statusCode < 200 || statusCode > 299) {
        if (retries > 0 && retryCodes.includes(statusCode)) {
          /* 3 */
          setTimeout(() => {
            return retryGet(url, retries - 1, backoff * 2)
          }, backoff)
        } else {
          reject(Error(res))
        }
      } else {
        res.on("data", d => {
          data += d
        })
        res.on("end", () => {
          resolve(data)
        })
      }
    })
  })
}
```

This is similar to the `fetch` example. First, set up the new arguments (1). Then, define the `retryCodes` (2). Finally, set up the retry logic and _return_ `retryGet`. This ensures that when the user calls `retryGet(...)` and expects a promise back, they will receive it.

## Wrapping up

You stuck with it through the bonus section 🎉! Using the same concepts in this article, you can apply retry functionality to your favorite library if it doesn't already include it. Looking for something more substantial? Give [Bearer](https://bearer.sh) a try, and check out with the [Bearer Blog](https://blog.bearer.sh/) for more on Node.js, API integrations, monitoring best practices, and more.

<!--kg-card-end: markdown-->