Fetch Smartly
Fetch Smartly is a zero-dependency, isomorphic HTTP client that wraps the native `fetch` API to provide production-grade resilience and intelligence for Node.js 18+, browsers, Cloudflare Workers, Deno, and Bun environments. It addresses common pain points of raw `fetch` by incorporating intelligent retry mechanisms with exponential backoff and jitter, respecting `Retry-After` headers, and automatically avoiding retries for 4xx client errors. The library offers a comprehensive typed error hierarchy, including `NetworkError`, `TimeoutError`, `HttpError`, and `RateLimitError`, enabling granular error handling via `instanceof` checks. Key features also include an automatic circuit breaker for failure isolation, request deduplication for identical concurrent GET/HEAD requests, and an offline queue with pluggable storage for replaying failed requests. Currently at version 1.0.2, the package is actively maintained with recent minor updates, distinguishing itself through its lightweight nature, strict TypeScript support, and robust, built-in resilience features.
Common errors
-
TypeError: fetch is not a function
cause The environment where `fetch-smartly` is running does not expose a global `fetch` API.fixEnsure you are running in a Node.js environment version 18 or higher, a modern browser, or a compatible serverless environment. If using an older Node.js, consider polyfilling `fetch` (though not recommended for `fetch-smartly` as it expects native behavior). -
CircuitBreakerOpenError: Circuit breaker is currently open. Request was short-circuited.
cause The circuit breaker mechanism has detected a high rate of failures and has 'opened' to prevent further requests from overloading the failing service, protecting both the client and the service.fixThis is expected behavior for a failing service. Wait for the `circuitBreaker.resetTimeout` to elapse, which will attempt to transition the circuit breaker to 'half-open' to test the service. Investigate and resolve the underlying service instability. -
FetchSmartlyConfigError: 'url' is a required option in the fetchWithRetry configuration.
cause The `url` property, which specifies the target URL for the HTTP request, was omitted from the configuration object passed to `fetchWithRetry`.fixAlways provide a valid URL string via the `url` property in the configuration object, e.g., `{ url: 'https://api.example.com/data' }`.
Warnings
- gotcha The `offlineQueue` feature requires a pluggable storage mechanism (e.g., localStorage or a custom adapter). If no storage is configured, requests queued during offline periods will be lost upon application restart or page refresh.
- gotcha While `fetch-smartly` aims for isomorphism, subtle differences in the underlying `fetch` implementation across various JavaScript runtimes (e.g., Node.js, browsers, Cloudflare Workers, Deno, Bun) may still lead to minor behavioral variances, especially concerning stream handling or error reporting specifics.
- gotcha The default retry policy will *not* retry 4xx client errors by default (unless specifically overridden by `shouldRetry`). Ensure that non-idempotent requests (e.g., POST, PUT, PATCH, DELETE) are handled carefully if `shouldRetry` is customized, to prevent unintended side effects on repeated execution.
Install
-
npm install fetch-smartly -
yarn add fetch-smartly -
pnpm add fetch-smartly
Imports
- fetchWithRetry
const fetchWithRetry = require('fetch-smartly');import { fetchWithRetry } from 'fetch-smartly'; - HttpError, NetworkError, TimeoutError
import { HttpError, NetworkError, TimeoutError } from 'fetch-smartly'; - FetchSmartlyConfig
import type { FetchSmartlyConfig } from 'fetch-smartly';
Quickstart
import { fetchWithRetry, HttpError, NetworkError, TimeoutError, RateLimitError } from 'fetch-smartly';
async function fetchDataWithResilience() {
try {
const response = await fetchWithRetry({
url: 'https://api.github.com/users/octocat', // Example public API endpoint
method: 'GET',
retry: {
attempts: 5,
delay: 'exponential', // Use exponential backoff
maxDelay: 5000, // Cap retry delay at 5 seconds
shouldRetry: (error) => !(error instanceof HttpError && error.status >= 400 && error.status < 500), // Don't retry client errors
},
timeout: 3000, // 3-second timeout for the request
circuitBreaker: {
threshold: 3, // Open after 3 consecutive failures
resetTimeout: 10000, // Try to close after 10 seconds
},
headers: {
'Content-Type': 'application/json',
'User-Agent': 'fetch-smartly-example',
'Authorization': `Bearer ${process.env.API_KEY ?? ''}` // Example of using an environment variable for auth
}
});
if (response.ok) {
const data = await response.json();
console.log('Fetched data:', data);
} else {
console.error('HTTP Error:', response.status, response.statusText);
}
} catch (error) {
if (error instanceof HttpError) {
console.error(`Request failed with HTTP status ${error.status}: ${error.message}`);
if (error instanceof RateLimitError) {
console.warn(`Rate limited! Retry-After: ${error.retryAfter} seconds`);
}
} else if (error instanceof NetworkError) {
console.error('Network connection error:', error.message);
} else if (error instanceof TimeoutError) {
console.error('Request timed out:', error.message);
} else {
console.error('An unexpected error occurred:', error);
}
}
}
fetchDataWithResilience();