npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

fetch-retryable

v1.1.2

Published

Simple wrapper around fetch to enable custom retry behaviour based on statusCode

Downloads

75

Readme

Fetch-Retryable

Fetch-Retryable is a simple utility module which wraps fetch (both in the browser and in node) in order to specify custom retry behaviours depending upon response statusCode and/or type of exception thrown.

Often, when working with third-party APIs, one needs the ability to retry a failed request in different ways, depending on the response to that request. For example, a 503-Service Unavailable error is (typically) infinitely retryable. On the other hand, 400-Bad Request is typically because of a client error, and so a retry would not be helpful. Similarly, many implementations of API rate-limiting will explicitly tell the client how long to wait before rate-limiting. Other APIs may request that clients implement exponential-backoff in their retry behaviour.

It's a complicated world.

Enter fetch-retryable. This simple module wraps fetch both in-browser and at the server, and gives the caller the ability to configure custom retry behaviour.

Quickstart

Fetch-Retryable depends upon isomorphic-fetch, which itself depends upon fetch or node-fetch to enable fetch on both client or server. Take a look at their documentation for details on making fetch requests.

Fetch-retryable (fetchRetryable(url, options)) is a drop-in replacement for fetch. In addition to the options specified in your fetch call, there is one more option it enables: retryOptions.

retryOptions gives you the ability to set maxRetries and retryTimeout for all responses, per statusCode class, or per statusCode. Here's a simple example:

import fetchRetryable from 'fetch-retryable';
const response = await fetchRetryable('https://google.ca', {
  method: 'get',
  retryOptions: {
    retryTimeout: 100,
    maxRetries: 3,
    status_503: {
      retryTimeout: 5000,
      maxRetries: 10
    }
  }
})

In the above example, we're setting both the default retry (max:3, timeout: 100), and we're setting a specific retry handler for 503 statusCodes (max: 10, timeout: 5000).

What else can I do?

I'm glad you asked.

Status code, status code class, and default retry behaviours

In the retryOptions field, there are three classes of retry options you can provide:

const response = await fetchRetryable('https://google.ca', {
  method: 'get',
  retryOptions: {
    retryTimeout: 100,            // Default behaviour
    maxRetries: 3,
    status_503: {                 // Retry behaviour for 503 errors only
      retryTimeout: 5000,
      maxRetries: 10
    },
    status_5xx: {                 // Behaviour for all 5xx class errors
      retryTimeout: 100,
      maxRetries: 2
    }
  }
})

As you can see above, you can provide behaviour that catches specific status codes, status code classes (eg status_5xx), and default. fetchRetryable tests in order of specificity, meaning status_503 beats status_5xx which beats the default retry options.

Custom retry timeout function

As we pointed out earlier, it's a complicated world, so to make things easier, fetchRetryable allows you to provide a custom retry timeout function.

That function should match the following signature: retryTimeout(retryContext)

retryContext is an object containing information about the current retry. It will always include a field retry: 1 indicating which retry (zero-based) is coming up. Additionally, it may contain a response object (the response object returned from the fetch call, from which you can get response.statusCode) or an error object (if an exception was thrown)

Important: It's probably not a terrific idea to, for example, await response.json() in your retryTimeout handler. You can only retrieve the body of a response once.

Example (retry-after):

const response = await fetchRetryable('https://google.ca', {
  method: 'get',
  retryOptions: {
    retryTimeout: 100,            // Default behaviour
    maxRetries: 3,
    status_429: {                 // Retry behaviour for 429 errors only
      retryTimeout: async (retryContext) => {
        const retryAfter = retryContext.response.headers.get('retry-after')
        await new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve()
          }, retryAfter * 1000)
        })
      },
      maxRetries: 10
    }
  }
})

In the example above, the 429-Too Many Requests statusCode response includes the header 'retry-after', which specifies how long the server expects the client to wait. When that statusCode is received, the retryTimeout function is called, the header is inspected, and the client waits before retrying.

Example (exponential backoff):

const response = await fetchRetryable('https://google.ca', {
  method: 'get',
  retryOptions: {
    retryTimeout: 100,            // Default behaviour
    maxRetries: 3,
    status_429: {                 // Retry behaviour for 429 errors only
      retryTimeout: async (retryContext) => {
        const initialRetryTimeout = 100;  // the timeout of the first retry.
        const timeout = (Math.pow(2, retryContext.retry)) * 100;
        await new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve()
          }, timeout)
        })
      },
      maxRetries: 10
    }
  }
})

In this example, our retry handler does not look at the retry-after field. Instead, it implements exponential backoff starting at 100ms. That means: retry 1 will wait 100ms. Retry 2 will wait 200ms. Retry 3 will wait 400ms. Retry 4 waits 800ms, and so on.