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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qfetch/middleware-retry-after

v0.0.0-reserved.0

Published

Fetch middleware for RFC 9110-compliant retry-after handling.

Readme

@qfetch/middleware-retry-after

Fetch middleware that automatically retries requests based on server-provided Retry-After headers with configurable backoff strategies.

Overview

Implements automatic retry logic following RFC 9110 §10.2.3 and RFC 6585 §4 semantics for 429 (Too Many Requests) and 503 (Service Unavailable) responses. When the server responds with these status codes and a valid Retry-After header, this middleware automatically parses the delay value and retries the request.

The middleware uses configurable backoff strategies from @proventuslabs/retry-strategies to add optional jitter to server-requested delays and control retry limits. The total wait time is the sum of the server's Retry-After delay plus the strategy's backoff value.

Intended for use with the composable middleware system provided by @qfetch/core.

Important Limitations

Replayable vs. Non-Replayable Bodies
Most request bodies — such as strings, Blob, ArrayBuffer, Uint8Array, FormData, and URLSearchParams — are replayable. Fetch recreates their internal body stream for each retry attempt, so these requests can be retried safely without any special handling.

Non-Replayable Bodies (Streaming Bodies)
Requests whose body is a non-replayable type — such as ReadableStreamcannot be retried according to the Fetch specification. Attempting to retry such requests results in a TypeError because the body stream has already been consumed.

To support retries for streaming bodies, you must provide a body factory using a middleware downstream that creates a fresh stream for each retry.

Installation

npm install @qfetch/middleware-retry-after @proventuslabs/retry-strategies

API

withRetryAfter(options)

Creates a middleware that retries failed requests based on the Retry-After header.

Options

  • strategy: () => BackoffStrategy (required) - Factory function that creates a backoff strategy for retry delays
    • The strategy determines additional delay (jitter) to add to the server-requested Retry-After delay
    • Controls when to stop retrying by returning NaN
    • Total wait time = Retry-After value + strategy backoff value
    • A new strategy instance is created for each request chain
    • Use upto() wrapper from @proventuslabs/retry-strategies to limit retry attempts
    • Common strategies: zero() (no jitter), fullJitter(), linear(), exponential()
  • maxServerDelay?: number - Maximum delay in milliseconds accepted from the server for a single retry (default: unlimited)
    • 0 means only allow instant retries (zero delay)
    • Positive integers set a ceiling on the server's requested delay
    • Negative or NaN values mean unlimited delay
    • If the server's Retry-After value exceeds this, a ConstraintError is thrown
  • retryableStatuses?: ReadonlySet<number> - Set of HTTP status codes that trigger automatic retries (default: new Set([429, 503]))
    • Only responses with these status codes and a valid Retry-After header will be retried
    • Override to customize which status codes should trigger retry behavior
    • Common additional codes: 502 (Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout)
    • Use an empty set (new Set()) to disable all automatic retries

Behavior

  • Successful responses (status 2xx) are returned immediately, even with a Retry-After header
  • Retryable statuses - By default, only 429 Too Many Requests or 503 Service Unavailable trigger retry logic when a valid Retry-After header is present. This can be customized using the retryableStatuses option
  • Retry-After parsing - Supports both formats specified in RFC 9110:
    • Delay-seconds: Integer values are interpreted as seconds to wait (e.g., "120")
    • HTTP-date: IMF-fixdate timestamps are interpreted as absolute retry time (e.g., "Wed, 21 Oct 2015 07:28:00 GMT")
    • Past dates result in zero-delay retry (immediate retry)
    • Invalid or missing headers prevent retries (response returned as-is, no error thrown)
    • Values exceeding safe integer range are treated as invalid
  • Backoff strategy:
    • The strategy determines additional delay (jitter) to add to the server's requested delay
    • Total wait time = Retry-After value + strategy backoff value
    • Strategy controls when to stop retrying by returning NaN
    • Use upto() wrapper to limit the number of retry attempts
    • Common strategies include zero() (no jitter), fullJitter(), linear(), and exponential()
  • Request body cleanup:
    • Automatically cancels the response body stream before retrying to prevent memory leaks
    • This is a best-effort operation that won't block retries if cancellation fails
  • Error handling:
    • Exceeding maxServerDelay throws a DOMException with name "ConstraintError"
    • Exceeding INT32_MAX (2,147,483,647 milliseconds or ~24.8 days) for total delay throws a RangeError
    • When the strategy returns NaN, retrying stops and the last response is returned (no error thrown)
  • Cancellation support:
    • Respects AbortSignal passed via request options or Request object
    • Cancellation during retry wait period immediately aborts and throws the abort reason
    • Cancellation during retry request execution propagates the abort signal to the fetch call

Usage

Basic usage (respect server delay exactly)

import { withRetryAfter } from '@qfetch/middleware-retry-after';
import { zero } from '@proventuslabs/retry-strategies';

const qfetch = withRetryAfter({
  strategy: () => zero() // No jitter, respect server delay exactly
})(fetch);

const response = await qfetch('https://api.example.com/data');

With limited retries

import { withRetryAfter } from '@qfetch/middleware-retry-after';
import { upto, zero } from '@proventuslabs/retry-strategies';

const qfetch = withRetryAfter({
  strategy: () => upto(3, zero()) // Maximum 3 retries
})(fetch);

const response = await qfetch('https://api.example.com/data');

With jitter (recommended to prevent thundering herd)

import { withRetryAfter } from '@qfetch/middleware-retry-after';
import { fullJitter, upto } from '@proventuslabs/retry-strategies';

const qfetch = withRetryAfter({
  strategy: () => upto(3, fullJitter(100, 10_000))
})(fetch);

const response = await qfetch('https://api.example.com/data');

With custom retryable status codes

import { withRetryAfter } from '@qfetch/middleware-retry-after';
import { fullJitter, upto } from '@proventuslabs/retry-strategies';

// Retry on 429 (Too Many Requests), 502 (Bad Gateway), and 520 (Cloudflare Unknown Error)
const qfetch = withRetryAfter({
  strategy: () => upto(3, fullJitter(100, 10_000)),
  retryableStatuses: new Set([429, 502, 520])
})(fetch);

const response = await qfetch('https://api.example.com/data');

With maximum server delay constraint

import { withRetryAfter } from '@qfetch/middleware-retry-after';
import { fullJitter, upto } from '@proventuslabs/retry-strategies';

const qfetch = withRetryAfter({
  strategy: () => upto(3, fullJitter(100, 10_000)),
  maxServerDelay: 120_000 // Maximum 2 minutes delay
})(fetch);

const response = await qfetch('https://api.example.com/data');

Notes

  • Requests are retried with the exact same parameters (URL, method, headers, body, etc.)
  • Response bodies are automatically cancelled before retrying to prevent memory leaks
  • By default, the middleware only retries on 429 and 503 status codes with valid Retry-After headers (customizable via retryableStatuses option)
  • Use the zero() strategy to respect server delays exactly without adding jitter
  • Use the upto() wrapper to limit the number of retry attempts
  • See @proventuslabs/retry-strategies for available backoff strategies