{"id":33180264,"url":"https://github.com/fetch-kit/ffetch","last_synced_at":"2026-05-29T03:01:57.002Z","repository":{"id":309252400,"uuid":"1035623898","full_name":"fetch-kit/ffetch","owner":"fetch-kit","description":"TypeScript-first fetch wrapper with configurable timeouts, retries, and circuit-breaker baked in.","archived":false,"fork":false,"pushed_at":"2025-10-25T01:17:38.000Z","size":225,"stargazers_count":158,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-25T03:11:07.166Z","etag":null,"topics":["backoff","fetch","http-client","javascript","production-ready","typescript"],"latest_commit_sha":null,"homepage":"","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/fetch-kit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"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":{"github":["gkoos"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2025-08-10T19:40:40.000Z","updated_at":"2025-10-25T01:16:49.000Z","dependencies_parsed_at":"2025-09-06T14:17:47.541Z","dependency_job_id":"657665e1-5273-4bde-aaf4-e97477f87dc1","html_url":"https://github.com/fetch-kit/ffetch","commit_stats":null,"previous_names":["gkoos/ffetch","fetch-kit/ffetch"],"tags_count":29,"template":false,"template_full_name":null,"purl":"pkg:github/fetch-kit/ffetch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetch-kit%2Fffetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetch-kit%2Fffetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetch-kit%2Fffetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetch-kit%2Fffetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fetch-kit","download_url":"https://codeload.github.com/fetch-kit/ffetch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetch-kit%2Fffetch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":285511793,"owners_count":27184240,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-20T02:00:05.334Z","response_time":54,"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","fetch","http-client","javascript","production-ready","typescript"],"created_at":"2025-11-16T03:00:41.705Z","updated_at":"2026-05-29T03:01:56.994Z","avatar_url":"https://github.com/fetch-kit.png","language":"TypeScript","funding_links":["https://github.com/sponsors/gkoos"],"categories":["Built with TypeScript"],"sub_categories":["Libraries"],"readme":"![npm](https://img.shields.io/npm/v/@fetchkit/ffetch)\r\n![Downloads](https://img.shields.io/npm/dm/@fetchkit/ffetch)\r\n![GitHub stars](https://img.shields.io/github/stars/fetch-kit/ffetch?style=social)\r\n\r\n![Build](https://github.com/fetch-kit/ffetch/actions/workflows/ci.yml/badge.svg)\r\n![codecov](https://codecov.io/gh/fetch-kit/ffetch/branch/main/graph/badge.svg)\r\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/fetch-kit/ffetch/badge)](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch)\r\n\r\n![MIT](https://img.shields.io/npm/l/@fetchkit/ffetch)\r\n![bundlephobia](https://badgen.net/bundlephobia/minzip/@fetchkit/ffetch)\r\n![Types](https://img.shields.io/npm/types/@fetchkit/ffetch)\r\n\r\n# @fetchkit/ffetch\r\n\r\n**A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**\r\n\r\nffetch can wrap any fetch-compatible implementation (native fetch, node-fetch, undici, or framework-provided fetch), making it flexible for SSR, edge, and custom environments.\r\n\r\nffetch uses a plugin architecture for optional features, so you only include what you need.\r\n\r\n## Why ffetch\r\n\r\n- Keep native fetch ergonomics, add production safety (timeouts, retries, error strategy).\r\n- Keep your runtime flexibility (use global fetch or any fetch-compatible handler).\r\n- Keep your bundle lean – **~3kb minified** (optional plugins, zero runtime dependencies).\r\n\r\n## Table of Contents\r\n\r\n- [@fetchkit/ffetch](#fetchkitffetch)\r\n  - [Why ffetch](#why-ffetch)\r\n  - [Table of Contents](#table-of-contents)\r\n  - [Key Features](#key-features)\r\n    - [Built-in Plugins at a Glance](#built-in-plugins-at-a-glance)\r\n  - [What Problems Does ffetch Solve?](#what-problems-does-ffetch-solve)\r\n  - [Quick Start](#quick-start)\r\n    - [Install](#install)\r\n    - [Basic Setup](#basic-setup)\r\n    - [Production Setup with Plugins](#production-setup-with-plugins)\r\n    - [Why not only native fetch?](#why-not-only-native-fetch)\r\n    - [Common Recipes](#common-recipes)\r\n    - [Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)](#using-a-custom-fetchhandler-ssr-metaframeworks-or-polyfills)\r\n    - [Advanced Example](#advanced-example)\r\n    - [Custom Error Handling with `throwOnHttpError`](#custom-error-handling-with-throwonhttperror)\r\n  - [Documentation](#documentation)\r\n  - [Environment Requirements](#environment-requirements)\r\n    - [\"AbortSignal.any is not a function\"](#abortsignalany-is-not-a-function)\r\n  - [CDN Usage](#cdn-usage)\r\n  - [Deduplication Limitations](#deduplication-limitations)\r\n  - [Fetch vs. Axios vs. ky vs. `ffetch`](#fetch-vs-axios-vs-ky-vs-ffetch)\r\n  - [Try ffetch in Action](#try-ffetch-in-action)\r\n  - [Join the Community](#join-the-community)\r\n  - [Security](#security)\r\n  - [Contributing](#contributing)\r\n  - [License](#license)\r\n\r\n## Key Features\r\n\r\n- **Timeouts** – per-request or global\r\n- **Retries** – exponential backoff + jitter\r\n- **Abort-aware retries** – aborting during backoff cancels immediately\r\n- **Plugin architecture** – extensible lifecycle-based plugins for optional behavior\r\n- **Hooks** – logging, auth, metrics, request/response transformation\r\n- **Pending requests** – real-time monitoring of active requests\r\n- **Per-request overrides** – customize behavior on a per-request basis\r\n- **Universal** – Node.js, Browser, Cloudflare Workers, React Native\r\n- **Zero runtime deps** – ships as dual ESM/CJS\r\n- **Configurable error handling** – custom error types and `throwOnHttpError` flag to throw on HTTP errors\r\n- **Bulkhead plugin (optional, prebuilt)** – cap concurrency and queue depth per client instance\r\n- **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection\r\n- **Hedge plugin (optional, prebuilt)** – race parallel attempts to reduce tail latency\r\n- **Context ID plugin (optional, prebuilt)** – inject a stable context ID header across retries/hedges for correlation\r\n- **Deduplication plugin (optional, prebuilt)** – automatic deduping of in-flight identical requests\r\n- **Request shortcuts plugin (optional, prebuilt)** – call `client.get(url)` / `.post()` / `.put()` / `.patch()` / `.delete()` directly on the client\r\n- **Response shortcuts plugin (optional, prebuilt)** – call `client(url).json()` / `.text()` / `.blob()` directly on the request promise\r\n- **Download progress plugin (optional, prebuilt)** – stream download progress callbacks with bytes transferred and percentage\r\n\r\n**Built-in error classes:** `TimeoutError`, `RetryLimitError`, `CircuitOpenError`, `BulkheadFullError`, `HttpError`, `NetworkError`, `AbortError`\r\n\r\n### Built-in Plugins at a Glance\r\n\r\nAll plugins are tree-shakeable — import only what you use.\r\n\r\n- **dedupePlugin (optional)**: dedupe in-flight identical requests.\r\n- **bulkheadPlugin (optional)**: cap in-flight concurrency with optional queue backpressure.\r\n- **hedgePlugin (optional)**: race multiple attempts and cancel losers when a winner is found.\r\n- **circuitPlugin (optional)**: fail fast after repeated failures.\r\n- **contextIdPlugin (optional)**: inject a stable request context ID (for example in `x-context-id`) across retries and hedges.\r\n- **requestShortcutsPlugin (optional)**: HTTP method shortcuts on the client (`.get()` / `.post()` / `.put()` / `.patch()` / `.delete()` / `.head()` / `.options()`).\r\n- **responseShortcutsPlugin (optional)**: use `client(url).json()` / `.text()` / `.blob()` style parsing.\r\n- **downloadProgressPlugin (optional)**: stream download progress via `onProgress(progress, chunk)` callback.\r\n\r\n## What Problems Does ffetch Solve?\r\n\r\nffetch is ideal for:\r\n\r\n- **Microservices and REST APIs** with retry requirements and timeout control\r\n- **High-traffic client applications** that need in-flight deduplication and circuit breaker protection\r\n- **SSR and metaframework apps** that require runtime flexibility (custom fetch handlers for different environments)\r\n- **Type-safe request handling** with strong TypeScript support and zero runtime dependencies\r\n\r\n## Quick Start\r\n\r\nMigrating from v4? Start with the [migration guide](./docs/migration.md) before applying the examples below.\r\n\r\n### Install\r\n\r\n```bash\r\n# npm\r\nnpm install @fetchkit/ffetch\r\n\r\n# yarn\r\nyarn add @fetchkit/ffetch\r\n\r\n# pnpm\r\npnpm add @fetchkit/ffetch\r\n\r\n# bun\r\nbun add @fetchkit/ffetch\r\n```\r\n\r\n### Basic Setup\r\n\r\n```typescript\r\nimport { createClient } from '@fetchkit/ffetch'\r\n\r\ntype User = { id: number; name: string }\r\n\r\nconst api = createClient({ timeout: 5000, retries: 2 })\r\nconst response = await api('https://api.example.com/users')\r\n\r\nif (!response.ok) {\r\n  throw new Error(`Request failed: ${response.status}`)\r\n}\r\n\r\nconst users = (await response.json()) as User[]\r\n```\r\n\r\n### Production Setup with Plugins\r\n\r\n```typescript\r\nimport { createClient } from '@fetchkit/ffetch'\r\nimport { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'\r\nimport { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'\r\nimport { contextIdPlugin } from '@fetchkit/ffetch/plugins/context-id'\r\nimport { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'\r\nimport { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'\r\n\r\nconst api = createClient({\r\n  timeout: 10_000,\r\n  retries: 2,\r\n  plugins: [\r\n    // 1) Optional: dedupe identical in-flight requests\r\n    dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),\r\n    // 2) Optional: open the circuit after repeated failures\r\n    circuitPlugin({ threshold: 5, reset: 30_000 }),\r\n    // 3) Optional: inject stable correlation context IDs\r\n    contextIdPlugin(),\r\n    // 4) Optional: enable request-promise parsing shortcuts\r\n    responseShortcutsPlugin(),\r\n    // 5) Optional: enable client HTTP method shortcuts\r\n    requestShortcutsPlugin(),\r\n  ],\r\n})\r\n\r\nconst users = await api\r\n  .get('https://api.example.com/users')\r\n  .json\u003cArray\u003c{ id: number; name: string }\u003e\u003e()\r\n\r\nconst p1 = api('https://api.example.com/data')\r\nconst p2 = api('https://api.example.com/data')\r\nconst [res1, res2] = await Promise.all([p1, p2])\r\n```\r\n\r\nWhat this setup gives you:\r\n\r\n- **Operational safety**: retries with timeout defaults.\r\n- **Lower duplicate traffic (optional)**: concurrent identical requests share one in-flight call.\r\n- **Faster failure recovery (optional)**: circuit breaker blocks repeated failing calls.\r\n- **Better observability correlation (optional)**: stable request context IDs across retries and hedges.\r\n- **Cleaner request ergonomics (optional)**: `client.get(url)` / `.post(url, init)` style shortcuts.\r\n- **Cleaner parsing (optional)**: `client(url).json()` style shortcuts.\r\n\r\n### Why not only native fetch?\r\n\r\n- Native fetch is a great baseline, but production apps usually need retries and timeout control.\r\n- ffetch keeps the fetch model and adds optional resilience features.\r\n- You can keep strict native behavior and only opt into plugins you need.\r\n\r\n### Common Recipes\r\n\r\n```typescript\r\n// Throw on non-2xx/429 once retries are exhausted\r\nconst strict = createClient({ throwOnHttpError: true })\r\n\r\n// Use a custom fetch implementation (SSR/framework/runtime)\r\nimport nodeFetch from 'node-fetch'\r\nconst apiWithCustomHandler = createClient({ fetchHandler: nodeFetch })\r\n\r\n// Keep native Response flow (works with or without plugins)\r\nconst plainApi = createClient({ timeout: 5000 })\r\nconst response = await plainApi('https://api.example.com/health')\r\nconst text = await response.text()\r\n```\r\n\r\n### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)\r\n\r\n```typescript\r\n// Why this exists:\r\n// ffetch wraps whatever fetch-compatible function you provide.\r\n// This is useful when your runtime has a scoped/framework fetch,\r\n// or when Node needs an explicit fetch implementation.\r\n\r\nimport { createClient } from '@fetchkit/ffetch'\r\nimport nodeFetch from 'node-fetch'\r\n\r\n// Node.js example: provide node-fetch explicitly\r\nconst apiNode = createClient({\r\n  fetchHandler: nodeFetch,\r\n  timeout: 5000,\r\n})\r\nconst nodeResponse = await apiNode('https://api.example.com/data')\r\n\r\n// Framework example: pass the framework-scoped fetch\r\n// (e.g. the fetch passed into a request handler)\r\nasync function loadData(frameworkFetch: typeof fetch) {\r\n  const api = createClient({\r\n    fetchHandler: frameworkFetch,\r\n    timeout: 5000,\r\n  })\r\n\r\n  const response = await api('/internal/data')\r\n  return response.json()\r\n}\r\n```\r\n\r\nAll ffetch features (timeouts, retries, plugins, hooks) behave the same with a custom `fetchHandler`.\r\n\r\nWith `responseShortcutsPlugin()` enabled, request-promise shortcuts like `api(url).json()` also work the same.\r\n\r\n### Advanced Example\r\n\r\n```typescript\r\n// Production-ready client with error handling and monitoring\r\nimport { createClient } from '@fetchkit/ffetch'\r\nimport { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'\r\nimport { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'\r\n\r\nconst client = createClient({\r\n  timeout: 10000,\r\n  retries: 2,\r\n  fetchHandler: fetch, // Use custom fetch if needed\r\n  plugins: [\r\n    dedupePlugin({\r\n      hashFn: (params) =\u003e `${params.method}|${params.url}|${params.body}`,\r\n      ttl: 30_000,\r\n      sweepInterval: 5_000,\r\n    }),\r\n    circuitPlugin({\r\n      threshold: 5,\r\n      reset: 30_000,\r\n      onCircuitOpen: ({ request, reason }) =\u003e\r\n        console.warn('Circuit opened due to:', request.url, reason.type),\r\n      onCircuitClose: ({ request, response }) =\u003e\r\n        console.info('Circuit closed after:', request.url, response.status),\r\n    }),\r\n  ],\r\n  hooks: {\r\n    before: async (req) =\u003e console.log('→', req.url),\r\n    after: async (req, res) =\u003e console.log('←', res.status),\r\n    onError: async (req, err) =\u003e console.error('Error:', err.message),\r\n  },\r\n})\r\n\r\ntry {\r\n  const response = await client('/api/data')\r\n\r\n  // Check HTTP status manually (like native fetch)\r\n  if (!response.ok) {\r\n    console.log('HTTP error:', response.status)\r\n    return\r\n  }\r\n\r\n  const data = await response.json()\r\n  console.log('Active requests:', client.pendingRequests.length)\r\n} catch (err) {\r\n  if (err instanceof TimeoutError) {\r\n    console.log('Request timed out')\r\n  } else if (err instanceof RetryLimitError) {\r\n    console.log('Request failed after retries')\r\n  }\r\n}\r\n```\r\n\r\n### Custom Error Handling with `throwOnHttpError`\r\n\r\nNative `fetch`'s controversial behavior of not throwing errors for HTTP error status codes (4xx, 5xx) can lead to overlooked errors in applications. By default, `ffetch` follows this same pattern, returning a `Response` object regardless of the HTTP status code. However, with the `throwOnHttpError` flag, developers can configure `ffetch` to throw an `HttpError` for HTTP error responses, making error handling more explicit and robust. Note that this behavior is affected by retries and the circuit breaker - full details are explained in the [Error Handling documentation](./docs/errorhandling.md).\r\n\r\n## Documentation\r\n\r\n| Topic                                                        | Description                                                               |\r\n| ------------------------------------------------------------ | ------------------------------------------------------------------------- |\r\n| **[Complete Documentation](./docs/index.md)**                | **Start here** - Documentation index and overview                         |\r\n| **[API Reference](./docs/api.md)**                           | Complete API documentation and configuration options                      |\r\n| **[Plugin Architecture](./docs/plugins.md)**                 | Plugin lifecycle, custom plugin authoring, and integration patterns       |\r\n| **[Deduplication](./docs/deduplication.md)**                 | How deduplication works, hash config, optional TTL cleanup, limitations   |\r\n| **[Error Handling](./docs/errorhandling.md)**                | Strategies for managing errors, including `throwOnHttpError`              |\r\n| **[Advanced Features](./docs/advanced.md)**                  | Per-request overrides, pending requests, circuit breakers, custom errors  |\r\n| **[Production Operations](./docs/production-operations.md)** | Pre-deploy checklist, alerting baseline, and incident playbook            |\r\n| **[Hooks \u0026 Transformation](./docs/hooks.md)**                | Lifecycle hooks, authentication, logging, request/response transformation |\r\n| **[Usage Examples](./docs/examples.md)**                     | Real-world patterns: REST clients, GraphQL, file uploads, microservices   |\r\n| **[Compatibility](./docs/compatibility.md)**                 | Browser/Node.js support, polyfills, framework integration                 |\r\n\r\n## Environment Requirements\r\n\r\n`ffetch` works best with native `AbortSignal.any` support:\r\n\r\n- **Node.js 20.6+** (native `AbortSignal.any`)\r\n- **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)\r\n\r\nIf your environment does not support `AbortSignal.any` (Node.js \u003c 20.6, older browsers), you can still use ffetch by installing an `AbortSignal.any` polyfill. `AbortSignal.timeout` is optional because ffetch includes an internal timeout fallback. See the [compatibility guide](./docs/compatibility.md) for instructions.\r\n\r\n**Custom fetch support:**\r\nYou can pass any fetch-compatible implementation (native fetch, node-fetch, undici, SvelteKit, Next.js, Nuxt, or a polyfill) via the `fetchHandler` option. This makes ffetch fully compatible with SSR, edge, metaframework environments, custom backends, and test runners.\r\n\r\n#### \"AbortSignal.any is not a function\"\r\n\r\nSolution: Install a polyfill for `AbortSignal.any`\r\n\r\n```bash\r\nnpm install abort-controller-x\r\n```\r\n\r\n## CDN Usage\r\n\r\n```html\r\n\u003cscript type=\"module\"\u003e\r\n  import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'\r\n\r\n  const api = createClient({ timeout: 5000 })\r\n  const data = await api('/api/data').then((r) =\u003e r.json())\r\n\u003c/script\u003e\r\n```\r\n\r\n## Deduplication Limitations\r\n\r\n- Deduplication is **off** by default. Enable it via `plugins: [dedupePlugin()]`.\r\n- The default hash function is `dedupeRequestHash`, which handles common body types and skips deduplication for streams and FormData.\r\n- Optional stale-entry cleanup: `dedupePlugin({ ttl, sweepInterval })` enables map-entry eviction. TTL eviction only removes dedupe keys; it does not reject already in-flight promises.\r\n- **Stream bodies** (`ReadableStream`, `FormData`): Deduplication is skipped for requests with these body types, as they cannot be reliably hashed or replayed.\r\n- **Non-idempotent requests**: Use deduplication with caution for non-idempotent methods (e.g., POST), as it may suppress multiple intended requests.\r\n- **Custom hash function**: Ensure your hash function uniquely identifies requests to avoid accidental deduplication.\r\n\r\nSee [deduplication.md](./docs/deduplication.md) for full details.\r\n\r\n## Fetch vs. Axios vs. ky vs. `ffetch`\r\n\r\n| Feature              | Native Fetch                                            | Axios                          | ky                                            | ffetch                                                                                 |\r\n| -------------------- | ------------------------------------------------------- | ------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |\r\n| Timeouts             | ❌ Manual AbortController                               | ✅ Built-in                    | ✅ Built-in                                   | ✅ Built-in with fallbacks                                                             |\r\n| Retries              | ❌ Manual implementation                                | ❌ Manual or plugins           | ✅ Built-in                                   | ✅ Smart exponential backoff                                                           |\r\n| Response Parsing DX  | ⚠️ Response methods only (`await fetch(...).then(...)`) | ✅ `response.data` convenience | ✅ `.json()/.text()/.blob()` on request chain | ✅ Optional `responseShortcutsPlugin()` (`.json()/.text()/.blob()` on request chain)   |\r\n| Plugin Architecture  | ❌ Not available                                        | ⚠️ Interceptors only           | ⚠️ Hook-based extensions                      | ✅ First-class plugin pipeline (optional built-in + custom plugins)                    |\r\n| Circuit Breaker      | ❌ Not available                                        | ❌ Manual or plugins           | ❌ Manual                                     | ✅ Automatic failure protection                                                        |\r\n| Deduplication        | ❌ Not available                                        | ❌ Not available               | ❌ Not available                              | ✅ Optional via `dedupePlugin()`                                                       |\r\n| Bulkheading          | ❌ Not available                                        | ❌ Not available               | ❌ Not available                              | ✅ Optional via `bulkheadPlugin()`                                                     |\r\n| Request Hedging      | ❌ Not available                                        | ❌ Not available               | ❌ Not available                              | ✅ Optional via `hedgePlugin()` (tail latency reduction)                               |\r\n| Request Monitoring   | ❌ Manual tracking                                      | ❌ Manual tracking             | ❌ Manual tracking                            | ✅ Built-in pending requests                                                           |\r\n| Error Types          | ❌ Generic errors                                       | ⚠️ HTTP errors only            | ✅ Specific error classes                     | ✅ Specific error classes                                                              |\r\n| TypeScript           | ⚠️ Basic types                                          | ⚠️ Basic types                 | ✅ Strong types                               | ✅ Full type safety                                                                    |\r\n| Hooks/Middleware     | ❌ Not available                                        | ✅ Interceptors                | ✅ Hooks                                      | ✅ Comprehensive lifecycle hooks                                                       |\r\n| Bundle Size          | ✅ Native (0kb)                                         | ❌ ~13kb minified              | ✅ Lightweight (fetch-based)                  | ✅ ~3kb minified                                                                       |\r\n| Modern APIs          | ✅ Web standards                                        | ❌ XMLHttpRequest              | ✅ Fetch + modern APIs                        | ✅ Fetch + modern features                                                             |\r\n| Download Progress    | ❌ Manual ReadableStream                                | ❌ Manual                      | ✅ `onDownloadProgress` callback              | ✅ Optional via `downloadProgressPlugin()`                                             |\r\n| Custom Fetch Support | ❌ No (global only)                                     | ❌ No                          | ❌ No                                         | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |\r\n\r\nNote: built-in plugins in ffetch are opt-in. Use `bulkheadPlugin()` for concurrency isolation and backpressure, `dedupePlugin()` for deduplication, `circuitPlugin()` for circuit breaking, `hedgePlugin()` for tail-latency racing, `requestShortcutsPlugin()` for client HTTP method shortcuts, `responseShortcutsPlugin()` for request-promise parsing shortcuts, and `downloadProgressPlugin()` for streaming download progress. Bundle size: ~3kb core, additional optional plugin imports are tree-shakeable.\r\n\r\n## Try ffetch in Action\r\n\r\nWant to see these clients in practice? Check out [ffetch-demo](https://github.com/fetch-kit/ffetch-demo) for working examples and side-by-side comparisons of how ffetch simplifies common fetch patterns.\r\n\r\n📰 Featured in [Node Weekly #594](https://nodeweekly.com/issues/594)\r\n\r\n## Join the Community\r\n\r\nGot questions, want to discuss features, or share examples? Join the **Fetch-Kit Discord server**:\r\n\r\n[![Discord](https://img.shields.io/badge/Discord-Join_Fetch--Kit-7289DA?logo=discord\u0026logoColor=white)](https://discord.gg/sdyPBPCDUg)\r\n\r\n## Security\r\n\r\n`ffetch` is scored at **7.4/10** by the [OpenSSF Scorecard](https://securityscorecards.dev/), an automated security health check for open source projects. This score reflects the commitment to security best practices and continuous improvement. Key security features include:\r\n\r\n- ✅ Pinned GitHub Actions dependencies\r\n- ✅ CodeQL static analysis on every PR and push to main\r\n- ✅ Dependabot for dependency updates and security alerts\r\n- ✅ npm publish with OIDC provenance attestations\r\n- ✅ Security policy and private vulnerability reporting\r\n- ✅ Branch protection on `main`\r\n- ✅ SPDX SBOM attached to every release\r\n\r\nThe score is capped below 10 due to being a solo-maintained project (no mandatory code review or multiple org contributors). [View the full breakdown](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch).\r\n\r\nTo report a security vulnerability, see [SECURITY.md](./SECURITY.md).\r\n\r\n## Contributing\r\n\r\n- **Issues**: [GitHub Issues](https://github.com/fetch-kit/ffetch/issues)\r\n- **Pull Requests**: [GitHub PRs](https://github.com/fetch-kit/ffetch/pulls)\r\n- **Documentation**: Found in `./docs/` - PRs welcome!\r\n\r\n## License\r\n\r\nMIT © 2025- gkoos\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffetch-kit%2Fffetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffetch-kit%2Fffetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffetch-kit%2Fffetch/lists"}