{"id":50901683,"url":"https://github.com/didrod205/fetchwise","last_synced_at":"2026-06-16T03:04:40.956Z","repository":{"id":361981644,"uuid":"1253083479","full_name":"didrod205/fetchwise","owner":"didrod205","description":"Tiny zero-dependency resilient fetch — retries, exponential backoff, timeouts \u0026 Retry-After. ~1KB, works everywhere fetch does.","archived":false,"fork":false,"pushed_at":"2026-06-02T03:38:53.000Z","size":47,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T05:17:43.633Z","etag":null,"topics":["backoff","exponential-backoff","fetch","fetch-retry","http","retries","retry","timeout","typescript","zero-dependency"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/fetchwise","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/didrod205.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"custom":["https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010"]}},"created_at":"2026-05-29T06:21:48.000Z","updated_at":"2026-06-02T03:38:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/didrod205/fetchwise","commit_stats":null,"previous_names":["didrod205/fetchwise"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/didrod205/fetchwise","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Ffetchwise","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Ffetchwise/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Ffetchwise/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Ffetchwise/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/didrod205","download_url":"https://codeload.github.com/didrod205/fetchwise/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/didrod205%2Ffetchwise/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34388681,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backoff","exponential-backoff","fetch","fetch-retry","http","retries","retry","timeout","typescript","zero-dependency"],"created_at":"2026-06-16T03:04:40.880Z","updated_at":"2026-06-16T03:04:40.938Z","avatar_url":"https://github.com/didrod205.png","language":"TypeScript","funding_links":["https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# fetchwise\n\n**A tiny, zero-dependency resilient `fetch` — retries, exponential backoff, timeouts and `Retry-After`, in ~1 KB gzipped.**\n\n[![npm version](https://img.shields.io/npm/v/fetchwise.svg?color=success)](https://www.npmjs.com/package/fetchwise)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/fetchwise?label=gzip)](https://bundlephobia.com/package/fetchwise)\n[![CI](https://github.com/didrod205/fetchwise/actions/workflows/ci.yml/badge.svg)](https://github.com/didrod205/fetchwise/actions/workflows/ci.yml)\n[![types](https://img.shields.io/npm/types/fetchwise.svg)](https://www.npmjs.com/package/fetchwise)\n[![license](https://img.shields.io/npm/l/fetchwise.svg)](./LICENSE)\n\n\u003c/div\u003e\n\n`fetchwise` wraps the **native `fetch`** you already use and makes it survive flaky\nnetworks and overloaded servers — without changing the API you know.\n\n```ts\nimport { fetchwise } from \"fetchwise\";\n\n// Exactly like fetch, but it retries transient failures automatically.\nconst res = await fetchwise(\"https://api.example.com/users\");\nconst users = await res.json();\n```\n\n---\n\n## Why fetchwise?\n\n- 🪶 **Zero dependencies.** ~1 KB gzipped. Nothing to audit, nothing to bloat your bundle.\n- 🔁 **Smart retries.** Exponential backoff with full jitter, configurable per request.\n- ⏱️ **Per-attempt timeouts.** Built on `AbortController` — no hanging requests.\n- 🚦 **Respects `Retry-After`.** Honors the server's own backoff hints on `429` / `503`.\n- 🌍 **Runs everywhere.** Node 18+, Deno, Bun, Cloudflare Workers and the browser — anywhere `fetch` exists.\n- 🧩 **Drop-in.** Same signature as `fetch`. Add resilience by passing one extra `retry` option.\n- 🛡️ **Type-safe.** Written in TypeScript, ships full type declarations.\n\n## Install\n\n```bash\nnpm install fetchwise\n# or: pnpm add fetchwise  /  yarn add fetchwise  /  bun add fetchwise\n```\n\nNo build step needed — it ships ESM **and** CommonJS.\n\n```ts\nimport { fetchwise } from \"fetchwise\";       // ESM / TypeScript\nconst { fetchwise } = require(\"fetchwise\");  // CommonJS\n```\n\n## Usage\n\n### Drop-in replacement\n\nAnywhere you call `fetch`, call `fetchwise` instead. By default it makes up to\n**4 attempts** (1 + 3 retries) on network errors and on the status codes\n`408, 425, 429, 500, 502, 503, 504`.\n\n```ts\nconst res = await fetchwise(\"/api/data\");\n```\n\n### JSON in one call\n\n`fetchJson` retries like `fetchwise`, **throws `HttpError` on a non-2xx** (so you\nnever parse an error page by mistake), and returns typed JSON. Pass `json` to\nserialize a body and set `Content-Type` automatically.\n\n```ts\nimport { fetchJson, HttpError } from \"fetchwise\";\n\nconst user = await fetchJson\u003cUser\u003e(\"/api/users/1\");\nconst created = await fetchJson\u003cUser\u003e(\"/api/users\", { json: { name: \"Ada\" } });\n\ntry {\n  await fetchJson(\"/api/secret\");\n} catch (e) {\n  if (e instanceof HttpError) console.log(e.status, e.response.url);\n}\n```\n\n### Tune the retry behavior\n\n```ts\nconst res = await fetchwise(\"/api/data\", {\n  method: \"POST\",\n  body: JSON.stringify({ hello: \"world\" }),\n  headers: { \"content-type\": \"application/json\" },\n  retry: {\n    retries: 5,        // up to 6 attempts total\n    minDelay: 200,     // first backoff (ms)\n    maxDelay: 10_000,  // cap (ms)\n    timeout: 4_000,    // abort \u0026 retry any attempt slower than 4s\n    onRetry: ({ attempt, delay, error, response }) =\u003e {\n      console.warn(`retry #${attempt} in ${delay}ms`, error ?? response?.status);\n    },\n  },\n});\n```\n\n### Create a preconfigured client\n\nShare defaults across your whole app — per-call options are merged on top.\n\n```ts\nimport { create } from \"fetchwise\";\n\nconst api = create({ retries: 5, timeout: 5_000 });\n\nawait api(\"/users\");                       // uses the defaults\nawait api(\"/report\", { retry: { retries: 0 } }); // override per call\n```\n\n### Cancel with an AbortSignal\n\nExternal aborts are respected immediately and are **never** retried.\n\n```ts\nconst res = await fetchwise(url, { signal: AbortSignal.timeout(10_000) });\n```\n\n### Retry only what you want\n\n```ts\nawait fetchwise(url, {\n  retry: {\n    // Custom status policy (array or predicate)\n    retryOnStatus: (status) =\u003e status \u003e= 500,\n    // Custom error policy — e.g. don't retry DNS failures\n    retryOnError: (err) =\u003e !String(err).includes(\"ENOTFOUND\"),\n  },\n});\n```\n\n## API\n\n### `fetchwise(input, init?) =\u003e Promise\u003cResponse\u003e`\n\nIdentical to `fetch(input, init)`, plus an optional `init.retry` object.\nReturns the final `Response`. Retryable statuses are returned as-is once retries\nare exhausted (it does **not** throw on HTTP errors — same as `fetch`).\n\n### `create(defaults?) =\u003e fetchwise`\n\nReturns a `fetchwise` function with the given `RetryOptions` baked in.\n\n### `RetryOptions`\n\n| Option              | Type                                              | Default                              | Description                                              |\n| ------------------- | ------------------------------------------------- | ------------------------------------ | -------------------------------------------------------- |\n| `retries`           | `number`                                          | `3`                                  | Additional attempts after the first failure.             |\n| `minDelay`          | `number`                                          | `300`                                | Base backoff delay in ms.                                |\n| `maxDelay`          | `number`                                          | `30000`                              | Maximum delay between attempts in ms.                    |\n| `factor`            | `number`                                          | `2`                                  | Exponential backoff multiplier.                          |\n| `jitter`            | `boolean`                                         | `true`                               | Apply full random jitter to each delay.                  |\n| `timeout`           | `number`                                          | `0` (off)                            | Per-attempt timeout in ms.                               |\n| `retryOnStatus`     | `number[] \\| (status, response) =\u003e boolean`       | `[408,425,429,500,502,503,504]`      | Which statuses to retry.                                 |\n| `retryOnError`      | `(error, context) =\u003e boolean`                     | retry all but external aborts        | Whether a thrown error is retryable.                     |\n| `respectRetryAfter` | `boolean`                                         | `true`                               | Honor the `Retry-After` response header.                 |\n| `onRetry`           | `(context) =\u003e void`                               | —                                    | Hook fired before each retry.                            |\n\n### Other exports\n\n- `TimeoutError` — thrown internally when a `timeout` elapses (retried like any error).\n- `parseRetryAfter(value, now?)` — parse a `Retry-After` header into milliseconds.\n\n## How backoff works\n\nEach retry waits `min(maxDelay, minDelay × factor^(attempt-1))`, then — with\n`jitter` on (the default) — a random value between `0` and that ceiling. Full\njitter spreads out retries from many clients so they don't stampede a recovering\nserver. If the response carries a `Retry-After` header, that value wins.\n\n## Comparison\n\n|                          | `fetchwise` | hand-rolled `try/catch` loop | heavier HTTP clients |\n| ------------------------ | :---------: | :--------------------------: | :------------------: |\n| Zero dependencies        |     ✅      |              ✅              |          ❌          |\n| Native `fetch` signature |     ✅      |              ⚠️              |          ❌          |\n| Backoff + jitter         |     ✅      |              ❌              |          ✅          |\n| `Retry-After` support    |     ✅      |              ❌              |         ⚠️           |\n| Per-attempt timeout      |     ✅      |              ❌              |          ✅          |\n| ~1 KB gzipped            |     ✅      |              —               |          ❌          |\n\n## Contributing\n\nContributions are very welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md)\nand our [Code of Conduct](./CODE_OF_CONDUCT.md). Good first issues are labeled\n[`good first issue`](https://github.com/didrod205/fetchwise/labels/good%20first%20issue).\n\n```bash\ngit clone https://github.com/didrod205/fetchwise.git\ncd fetchwise\nnpm install\nnpm test\n```\n\n## 💖 Sponsor\n\n`fetchwise` is free and MIT-licensed, built and maintained in spare time. If it\nsaves you from writing yet another retry loop, please consider supporting it —\nevery bit helps keep the project healthy and the issues answered.\n\n- ⭐ **Star this repo** — the simplest, free way to help others discover it.\n- 🍋 **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** — one-time or recurring support.\n\n\u003e Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏\n\n## License\n\n[MIT](./LICENSE) © fetchwise contributors\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdidrod205%2Ffetchwise","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdidrod205%2Ffetchwise","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdidrod205%2Ffetchwise/lists"}