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

safe-fetch-iq

v0.1.2

Published

Safe, zero-dependency fetch wrapper with smart retries, timeouts, dedupe, and caching.

Downloads

78

Readme

safe-fetch-iq – Smart, Safe HTTP Fetch for JS/TS

safe-fetch-iq is a small TypeScript library that makes fetch safe and ergonomic by default, powered internally by an intelligent retry engine.

  • Drop-in fetch replacement (import fetch from "safe-fetch-iq")
  • Smart defaults: retry, timeout, auto JSON/text parsing
  • Request deduplication and response caching
  • Built on native fetch (Node 18+ and browsers)
  • Zero runtime dependencies, tiny bundle size

Installation

npm install safe-fetch-iq

If you use TypeScript, typescript and @types/node should be available in your project.

Quick Start

import fetch from "safe-fetch-iq";

const users = await fetch("https://api.example.com/users");

console.log(users);

Default behavior:

  • Retries network and transient errors up to 3 times
  • Per-attempt timeout of 30 seconds
  • Automatically parses JSON or text based on Content-Type
  • Follows redirects using native fetch behavior
  • Throws descriptive errors instead of silent failures

Smart Options

safe-fetch-iq extends the standard RequestInit with a few focused options:

import fetch from "safe-fetch-iq";

const data = await fetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ foo: "bar" }),

  retry: 5,
  timeout: 10_000,
  retryOn: [429, 503],
  retryDelay: attempt => attempt * 1000,
  dedupe: true,
  cache: "5m"
});

Supported options:

  • retry?: number
    • Maximum retry attempts (default: 3)
  • timeout?: number
    • Per-attempt timeout in milliseconds (default: 30000)
  • retryOn?: number[]
    • HTTP status codes to retry (e.g. [429, 503])
  • retryDelay?: (attempt: number) => number
    • Custom backoff function for delay in ms
  • dedupe?: boolean
    • Request deduplication for identical in-flight GET requests (default: true)
  • cache?: number | string
    • Response cache TTL for GET requests
    • Number (ms) or string like "10s", "5m", "1h"

All regular fetch options still work. You can also pass an AbortSignal via signal as usual.

Request Deduplication

When dedupe is enabled (default), concurrent identical GET requests share the same promise:

import fetch from "safe-fetch-iq";

const p1 = fetch("https://api.example.com/users");
const p2 = fetch("https://api.example.com/users");

// p1 and p2 resolve with the same underlying network response
const [a, b] = await Promise.all([p1, p2]);

This avoids stampedes when multiple parts of your app request the same data at the same time.

Response Caching

For GET requests, you can enable simple in-memory caching:

import fetch from "safe-fetch-iq";

// Cache for 5 minutes
const data = await fetch("https://api.example.com/data", {
  cache: "5m"
});
  • The cache key is based on method+URL+body
  • Calls within the TTL return the cached parsed value
  • After TTL expires, a new request is made and cache is refreshed

Error Handling

safe-fetch-iq throws when:

  • The response is not ok (non-2xx), after applying retries
  • Retries are exhausted or cancelled by rules

Example:

import fetch, { RetryIQError } from "safe-fetch-iq";

try {
  const data = await fetch("https://api.example.com/data", {
    retry: 5,
    retryOn: [429, 503]
  });
  console.log(data);
} catch (err) {
  if (err instanceof RetryIQError) {
    console.error("Retry failed", err.metadata);
  } else {
    console.error("Unexpected error", err);
  }
}

RetryIQError includes metadata:

  • attempt – last attempt index
  • totalAttempts
  • errorType
  • elapsedMs

Non-OK HTTP responses are surfaced as errors that still carry the original Response for advanced handling.

Advanced Use: withRetry Engine

Under the hood, safe-fetch-iq is powered by a generic intelligent retry engine. You can use it directly for other clients like axios, custom HTTP clients, or even non-HTTP operations.

Core Idea

import { withRetry } from "safe-fetch-iq";

const result = await withRetry(async (attempt, signal) => {
  return doSomethingHttpLike(attempt, signal);
});

The engine decides when to retry, how long to wait, and when to give up.

Smart Behavior

1. Detects Retry-After

When the server returns 429 or 5xx with a Retry-After header, the engine parses the header and waits at least that long before the next attempt.

  • Retry-After: 10 is interpreted as 10 seconds
  • Retry-After: Wed, 21 Oct 2015 07:28:00 GMT is treated as an absolute time

If no Retry-After header exists, standard backoff is used.

2. Error-Aware Backoff

Errors are classified into types:

  • rateLimit – typically HTTP 429
  • serverError – HTTP 5xx
  • networkError – connection resets, timeouts, DNS errors
  • clientError4xx that should not be retried
  • unknown – anything else

By default, only rateLimit, serverError, networkError, and unknown are retried. Client errors are treated as final.

3. Budget-Aware Retries

You control how aggressive the retries can be.

import { withRetry } from "safe-fetch-iq";

const response = await withRetry(
  () => fetch("https://api.example.com/data"),
  {
    budget: {
      maxRetries: 5,
      maxRetryTimeMs: 60_000
    }
  }
);
  • maxRetries – maximum number of retry attempts
  • maxRetryTimeMs – hard cap on total time spent retrying

If the server sends rate-limit headers:

  • X-RateLimit-Remaining
  • X-Rate-Limit-Remaining

and they indicate that you are out of quota, Retry IQ stops early instead of blindly continuing to hit the API.

Backoff Configuration

import { withRetry } from "safe-fetch-iq";

await withRetry(
  () => fetch("https://api.example.com/data"),
  {
    backoff: {
      baseDelayMs: 250,
      maxDelayMs: 30_000,
      factor: 2,
      jitter: "full",
      perErrorType: {
        rateLimit: {
          baseDelayMs: 1000
        }
      }
    }
  }
);
  • baseDelayMs – first delay before backoff, default 250
  • factor – exponential factor, default 2
  • maxDelayMs – max per-attempt delay, default 30000
  • jitter
    • "none" – deterministic backoff
    • "full" – random between 0 and delay
    • "decorrelated" – random between delay / 2 and delay
  • perErrorType – override backoff settings per error type

shouldRetry Hook

Use shouldRetry to inject custom rules.

import { withRetry } from "safe-fetch-iq";

await withRetry(
  () => fetch("https://api.example.com/data"),
  {
    shouldRetry: (error, context) => {
      if (context.errorType === "clientError") return false;
      if (context.attempt > 3) return false;
      return true;
    }
  }
);

You receive both the error and detailed context, including the calculated next delay.

Logging

Pass a logger to understand how retries behave in production.

import { withRetry } from "safe-fetch-iq";

await withRetry(
  () => fetch("https://api.example.com/data"),
  {
    logger: event => {
      if (event.type === "retry") {
        console.log(
          `[RetryIQ] ${event.operationName ?? "operation"} attempt ${event.attempt} – waiting ${event.delayMs}ms`
        );
      }
      if (event.type === "giveUp") {
        console.warn(
          `[RetryIQ] giving up after ${event.attempt} attempts due to ${event.reason} (${event.errorType})`
        );
      }
      if (event.type === "succeeded") {
        console.log(
          `[RetryIQ] succeeded in ${event.elapsedMs}ms after ${event.attempt} attempts`
        );
      }
    }
  }
);

Using AbortSignal

You can cancel the entire retry sequence with an AbortSignal.

import { withRetry } from "safe-fetch-iq";

const controller = new AbortController();

const promise = withRetry(
  (attempt, signal) => fetch("https://api.example.com/data", { signal }),
  {
    signal: controller.signal
  }
);

setTimeout(() => controller.abort(), 5000);

await promise;

If the signal is aborted, Retry IQ stops immediately.

Error Handling

When retries are exhausted or cancelled by rules, the engine throws a RetryIQError.

import { withRetry, RetryIQError } from "safe-fetch-iq";

try {
  await withRetry(() => fetch("https://api.example.com/data"));
} catch (err) {
  if (err instanceof RetryIQError) {
    console.error("Retry failed", err.metadata);
  } else {
    console.error("Unexpected error", err);
  }
}

RetryIQError includes metadata:

  • attempt – last attempt index
  • totalAttempts
  • errorType
  • elapsedMs

Adapters

fetch adapter

If you prefer a dedicated wrapper on top of native fetch:

import { withRetryFetch } from "safe-fetch-iq";

const fetchWithRetry = withRetryFetch(fetch);

const response = await fetchWithRetry("https://api.example.com/data");

You can pass options on each call:

const response = await fetchWithRetry(
  "https://api.example.com/data",
  { method: "GET" },
  {
    budget: { maxRetries: 3 }
  }
);

axios adapter

import axios from "axios";
import { withRetryAxios } from "safe-fetch-iq";

const axiosClient = axios.create({ baseURL: "https://api.example.com" });

const axiosWithRetryFactory = withRetryAxios(axiosClient);
const axiosWithRetry = axiosWithRetryFactory({
  budget: { maxRetries: 4 }
});

const response = await axiosWithRetry.request({ method: "GET", url: "/users" });
console.log(response.data);

API Reference

withRetry(operation, options?)

Wraps any async operation with intelligent retry behavior.

  • operation(attempt, signal) – async function that performs the work
  • optionsRetryIQOptions

RetryIQOptions

  • operationName?: string
  • classifyError?: (error) => RetryErrorType
  • shouldRetry?: (error, context) => boolean | Promise<boolean>
  • budget?: RetryBudgetConfig
  • backoff?: RetryBackoffConfig
  • logger?: (event: RetryEvent) => void
  • respectRetryAfterHeader?: boolean (default true)
  • signal?: AbortSignal

Backoff and budget types

  • RetryBudgetConfig
    • maxRetries: number
    • maxRetryTimeMs?: number
  • RetryBackoffConfig
    • baseDelayMs?: number
    • maxDelayMs?: number
    • factor?: number
    • jitter?: "none" | "full" | "decorrelated"
    • perErrorType?: Partial<Record<RetryErrorType, Partial<Omit<RetryBackoffConfig, "perErrorType">>>>

When To Use safe-fetch-iq

  • You want a safer default fetch with retries and timeouts
  • You need simple request deduplication and caching without extra infra
  • You prefer zero-dependency utilities over heavyweight HTTP clients