{"id":15434525,"url":"https://github.com/paperstrike/onfetch","last_synced_at":"2026-04-16T11:01:48.008Z","repository":{"id":38440385,"uuid":"407609493","full_name":"PaperStrike/onfetch","owner":"PaperStrike","description":"Mock fetch() with native Request / Response API. Works with globalThis, service worker, @mswjs/interceptors, and custom contexts","archived":false,"fork":false,"pushed_at":"2023-01-08T01:43:14.000Z","size":862,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-21T11:49:46.762Z","etag":null,"topics":["browser","fetch","mock","nodejs","service-worker","testing"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PaperStrike.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":"PaperStrike","issuehunt":null,"otechie":null,"custom":null}},"created_at":"2021-09-17T16:28:41.000Z","updated_at":"2022-01-17T09:18:18.000Z","dependencies_parsed_at":"2023-02-08T04:16:09.813Z","dependency_job_id":null,"html_url":"https://github.com/PaperStrike/onfetch","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/PaperStrike/onfetch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaperStrike%2Fonfetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaperStrike%2Fonfetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaperStrike%2Fonfetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaperStrike%2Fonfetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PaperStrike","download_url":"https://codeload.github.com/PaperStrike/onfetch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PaperStrike%2Fonfetch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31882886,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T09:23:21.276Z","status":"ssl_error","status_checked_at":"2026-04-16T09:23:15.028Z","response_time":69,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["browser","fetch","mock","nodejs","service-worker","testing"],"created_at":"2024-10-01T18:39:49.220Z","updated_at":"2026-04-16T11:01:47.969Z","avatar_url":"https://github.com/PaperStrike.png","language":"TypeScript","funding_links":["https://liberapay.com/PaperStrike"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"end\"\u003e\u003csub\u003e\n  ENGLISH,\n  \u003ca title=\"Simplified Chinese\" href=\"README.zh-Hans.md\"\u003e简体中文\u003c/a\u003e\n\u003c/sub\u003e\u003c/div\u003e\n\n# onfetch\n[q-a]: https://github.com/PaperStrike/onfetch/discussions/categories/q-a\n[contributing]: https://github.com/PaperStrike/onfetch/blob/main/.github/CONTRIBUTING.md\n\n[mdn-fetch-func]: https://developer.mozilla.org/en-US/docs/Web/API/fetch\n[mdn-request-api]: https://developer.mozilla.org/en-US/docs/Web/API/Request\n[mdn-response-api]: https://developer.mozilla.org/en-US/docs/Web/API/Response\n\n[![Build Status](https://github.com/PaperStrike/onfetch/actions/workflows/test.yml/badge.svg)](https://github.com/PaperStrike/onfetch/actions/workflows/test.yml)\n[![npm Package](https://img.shields.io/npm/v/onfetch?logo=npm \"onfetch\")](https://www.npmjs.com/package/onfetch)\n\nMock `fetch()` with native [`Request`][mdn-request-api] / [`Response`][mdn-response-api] API. Works with [`globalThis`](#default), [service worker](#service-worker), [`@mswjs/interceptors`](#msw-interceptors), and [custom contexts](#custom-context).\n\n---\n\n🐿️ Jump to\n[Callback](#callback),\n[Delay](#delay),\n[Redirect](#redirect),\n[Times](#times),\n[Restore](#restore),\n[Context](#context),\n[Options](#options),\n[Q\u0026A][q-a],\nor\n[Contributing Guide][contributing].\n\n## Basic\n[mdn-headers-api]: https://developer.mozilla.org/en-US/docs/Web/API/Headers\n\nStart with `onfetch`, pass the same params as constructing a [`Request`][mdn-request-api] object. Then `reply` as constructing a [`Response`][mdn-response-api] object.\n\n```js\nimport onfetch from 'onfetch';\n\nonfetch('/simple').reply('path');\n\n// Or\nonfetch('/post', { method: 'POST' })\n  .reply('received');\n\n// Or\nonfetch('/down')\n  .reply(null, { status: 500 });\n```\n\nWorks with [`node-fetch`](https://github.com/node-fetch/node-fetch), [`whatwg-fetch`](https://github.com/github/fetch), [`cross-fetch`](https://github.com/lquixada/cross-fetch), whatever, and mainly, modern browsers.\n\nIn Node, in addition to setting up global [`fetch`][mdn-fetch-func], you also need to set up global [`Headers`][mdn-headers-api], [`Request`][mdn-request-api], and [`Response`][mdn-response-api].\n\n### Matching\n[mdn-request-body]: https://developer.mozilla.org/en-US/docs/Web/API/Request/body\n\nHow `onfetch` uses your params to match a [`Request`][mdn-request-api].\n\nTo keep this simple and efficient, we don't and won't support [`body`][mdn-request-body] matching. You will have to put your own processing code into a [reply callback](#callback) when needed.\n\nRules without a positive [`times`](#times) match no request.\n\n#### String\n[mdn-url-api]: https://developer.mozilla.org/en-US/docs/Web/API/URL\n\nA string matches the request's URL if all the following checks pass:\n\n1. Split into three parts. Path, query string and hash.\n2. Check if the path matches the request's path.\n3. For the query string and hash, if not empty, check if it matches the request's one.\n\n```js\nonfetch('/string').persist();\n\nfetch('/string'); // match\nfetch('/string?query'); // match\nfetch('/string#hash'); // match\n```\n\n```js\nonfetch('/string?specify-query').persist();\n\nfetch('/string'); // not match\nfetch('/string?other-query'); // not match\nfetch('/string?specify-query'); // match\nfetch('/string?specify-query#hash'); // match\n```\n\nThe use of [`persist()`](#persist) allows the above `onfetch` rules to match an unlimited number of times.\n\n#### RequestInit\n[idl-request-init]: https://fetch.spec.whatwg.org/#requestinit\n\nThe second param, a [`RequestInit`][idl-request-init] object, matches the [`Request`][mdn-request-api], when all the checks in the following steps pass:\n\n1. Deconstruct `headers`, `body`, `window` and the _rest parts_ from the [`RequestInit`][idl-request-init] object.\n2. Check if each header in `headers` has a match in the request's headers.\n3. Check if each part in the _rest parts_ has a match in the [`Request`][mdn-request-api].\n\n```js\nonfetch('', {\n  method: 'GET',\n  headers: {\n    'Content-Type': 'text/plain',\n  },\n}).persist();\n\n// not match\nfetch('', { method: 'GET' });\n\n// not match\nfetch('', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'text/plain',\n  },\n});\n\n// match\nfetch('', {\n  cache: 'no-cache',\n  method: 'GET',\n  headers: {\n    'Accept': 'text/html',\n    'Content-Type': 'text/plain',\n  },\n});\n```\n\n#### Objects with `test` method\n\nOther than using strings, you can also pass any object that has a `test` method as the first arg to test the request's URL.\n\nThe `test` method will receive the _full URL string_ and should return a boolean indicating the match result.\n\n##### RegExp\n[mdn-regexp-api]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp\n\nAn example that uses a [`RegExp`][mdn-regexp-api]:\n\n```js\n// Match URLs that ends with '.foo'.\nonfetch(/\\.foo$/).reply('bar');\n```\n\nBecause it tests the entire URL string, if this `onfetch` rule doesn't need to care about the query string or the hash, write it like:\n\n```js\n// Use regex that\n// allows any trailing query string and hash.\nonfetch(/^[^?#]*\\.foo([?#]|$)/).reply('bar');\n```\n\n##### URLPattern\n[mdn-url-pattern-api]: https://developer.mozilla.org/en-US/docs/Web/API/URLPattern\n\nAnother example that uses a [`URLPattern`][mdn-url-pattern-api]:\n\n```js\nconst pattern = new URLPattern('http{s}?://*.example.com/books/:id');\nonfetch(pattern);\n```\n\n#### Request\n\nYou can also pass a [`Request`][mdn-request-api] object as the first arg, to match the request in a manner similar with the [`RequestInit` matcher](#requestinit).\n\n## Callback\n[idl-body-init]: https://fetch.spec.whatwg.org/#bodyinit\n[mdn-promise-api]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise\n\nOther than `reply` as constructing a [`Response`][mdn-response-api], you can also pass a callback function to form the response.\n\nYour callback will receive two params, the first one points to the [`Request`][mdn-request-api] object, the second one gives you both the original and the mocked `fetch`.\n\nRemember to return a [`Response`][mdn-response-api], [`BodyInit`][idl-body-init], `null`, or a [`Promise`][mdn-promise-api] that resolves to one of them.\n\n```js\nonfetch('').reply((request, fetchers) =\u003e {\n  const example = request.headers.get('One');\n  if (example === 'original') {\n    return fetchers.original(request);\n  }\n  if (example === 'mocked') {\n    return fetchers.mocked('/mocked');\n  }\n  return 'default-response';\n});\n```\n\n### passThough\n\nA syntactic sugar for sending requests via the original `fetch`.\n\n```js\nimport onfetch, { passThrough } from 'onfetch';\nonfetch('/use-original').reply(passThrough);\n```\n\n## Delay\n\n```js\n// Delay 200ms before reply.\nonfetch('').delay(200).reply('');\n```\n\nThe order of `delay` and `reply` does not affect the result.\n\n```js\n// Same effect.\nonfetch('').reply('').delay(200);\n```\n\nThe delay duration accumulates.\n\n```js\n// Delay 400ms before reply.\nonfetch('').delay(200).delay(300).delay(-100).reply('');\n```\n\n## Redirect\n[redirect-status]: https://fetch.spec.whatwg.org/#redirect-status\n[mdn-response-redirect]: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect\n[mdn-request-redirect]: https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect\n[mdn-response-redirected]: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirected\n[mdn-response-url]: https://developer.mozilla.org/en-US/docs/Web/API/Response/url\n\nUse a [`Response`][mdn-response-api] object that has [a redirect status][redirect-status] to redirect requests. You can use [`Response.redirect`][mdn-response-redirect] to construct a such object.\n\n```js\n// Redirect to '/bar'.\nonfetch('/foo').reply(Response.redirect('/bar'));\n\n// `/bar` respond with `redirected`.\nonfetch('/bar').reply('redirected');\n\n// Logs 'redirected'\nfetch('/foo').then((res) =\u003e res.text()).then(console.log);\n```\n\n### Limitations\n\n- In [default mode](#default), redirecting a [`Request`][mdn-request-api] with [`redirect`][mdn-request-redirect] set to a value other than `follow` will fail the fetch with a `TypeError`.\n- In [default mode](#default), a redirected [`Response`][mdn-response-api] only has the correct [`redirected`][mdn-response-redirected] and [`url`][mdn-response-url] properties defined on the response object itself. Reading them via the prototype will give you incorrect values.\n\n## Times\n\nYou can specify the number of times to apply the `onfetch` rule via the `times` function. It accepts an integer as the number of applications of the rule.\n\n```js\n// Apply this rule 5 times.\nonfetch('/foo').times(5).reply('bar');\n```\n\nYou may have multiple rules matching a request at the same time, but only the first rule will apply.\n\nBy default, an `onfetch` rule only applies _once_. When the times ran out, it bypasses the match.\n\n```js\nonfetch('/foo').reply('alpha');\nonfetch('/foo').reply('beta');\n\n// Logs 'alpha'\nfetch('/foo').then((res) =\u003e res.text()).then(console.log);\n\n// Logs 'beta'\nfetch('/foo').then((res) =\u003e res.text()).then(console.log);\n```\n\nYou can specify the times at any time as long as you store the reference of the `onfetch` rule somewhere.\n\n```js\nconst onFoo = onfetch('/foo').reply('bar');\n\nfetch('/foo'); // match\n\n// Once again.\nonFoo.once();\n\nfetch('/foo'); // match\n```\n\nNote that when all the `onfetch` rules do not match a request, that request goes to [`options.defaultRule`](#default-rule).\n\nThe `times(n)` doesn't accumulate. It overrides.\n\n```js\nconst onFoo = onfetch('/foo').twice().once().reply('bar');\n\nfetch('/foo'); // match\nfetch('/foo'); // fallback to `defaultRule`\n```\n\n### `once()`\n\nA syntactic sugar for `rule.times(1)`.\n\n### `twice()`\n\nSyntactic sugar for `rule.times(2)`.\n\n### `thrice()`\n\nSugar for `rule.times(3)`.\n\n### `persist()`\n\nFor `rule.times(Infinity)`.\n\n## Restoration\n\n### Restore\n\n`restore` deactivates `onfetch` to stop intercepting HTTP calls. Note that it does not clear any intercept rules.\n\n```js\nonfetch.restore();\n```\n\n### Remove a single rule\n\n```js\nconst first = onfetch('/same').reply('first');\nonfetch('/same').reply('second');\n\nonfetch.remove(first);\n\n// Logs 'second'\nfetch('/foo').then((res) =\u003e res.text()).then(console.log);\n```\n\n### Remove all intercept rules\n\n```js\nonfetch.cleanAll();\n```\n\n### Activate\n\nTo (re-)activate `onfetch` to start intercepting HTTP calls, you can use `activate()`.\n\n`onfetch` activates itself when you first import it.\n\n```js\nonfetch.restore();\n\n// After some code.\n\nonfetch.activate();\n```\n\n## Context\n\n### Default\n[mdn-global-this-global]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis\n\nIn the default mode, `onfetch` intercepts calls to the `fetch()` method on [`globalThis`][mdn-global-this-global].\n\nTo switch back from another mode to this, run:\n\n```js\nawait onfetch.useDefault();\n```\n\n### Service Worker\n[mdn-service-worker-api]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API\n[mdn-xml-http-request-api]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n\n[Service Worker API][mdn-service-worker-api] only works in browsers.\n\nWith the help of [Service Worker API][mdn-service-worker-api], you can now mock **all** the resources your page requires, including those don't go with [XMLHttpRequest][mdn-xml-http-request-api] nor [`fetch`][mdn-fetch-func] (e.g., CSS files).\n\n```js\n// In the main browsing context.\nimport onfetch from 'onfetch';\n\nawait onfetch.useServiceWorker();\n\nonfetch('/script.js').reply('console.log(\\'mocked!\\')');\nconst script = document.createElement('script');\nscript.src = '/script.js';\n\n// Logs 'mocked!'\ndocument.head.append(script);\n```\n\nTo enable this feature, import `onfetch/sw` in your service worker.\n\n```js\n// In the service worker.\nimport 'onfetch/sw';\n```\n\nTo switch back to the standard mode in the client side, call `onfetch.useDefault()`.\n\nTo disable `onfetch/sw` in the worker side, store its default import value beforehand, and call its `restore` method.\n\n```js\n// In the service worker.\nimport onfetchWorker from 'onfetch/sw';\n\nself.addEventListener('message', async ({ data }) =\u003e {\n  // To re-activate, call `.activate()`.\n  if (data?.example) await onfetchWorker.restore();\n\n  // ...\n});\n```\n\n### MSW Interceptors\n[msw-interceptors]: https://github.com/mswjs/interceptors\n\n[`@mswjs/interceptors`][msw-interceptors] is a HTTP/HTTPS/XHR/fetch request interception library that can intercept most of the requests in Node.js.\n\nWith the help of [`@mswjs/interceptors`][msw-interceptors], we can mock almost all the resources our Node tests requires. To install, run:\n\n```shell\nnpm i @mswjs/interceptors --save-dev\n```\n\nThen somewhere in your test cases, call `useMSWInterceptors`.\n\n```js\n// Switch to @mswjs/interceptors mode.\nawait onfetch.useMSWInterceptors();\n```\n\n### Auto Advanced\n\nAuto Advanced mode uses either [service worker mode](#service-worker) or [msw interceptors mode](#msw-interceptors) depending on whether the env supports the [Service Worker API][mdn-service-worker-api].\n\n```js\n// Switch to service worker mode, if the env supports it.\n// Otherwise, switch to @mswjs/interceptors mode.\nawait onfetch.useAutoAdvanced();\n```\n\n### Custom Context\n\n`onfetch` works by replacing the `fetch` property of a given \"context\" with a mocked one. By default, this context refers to [`globalThis`][mdn-global-this-global]. The [service worker mode](#service-worker) and [msw interceptors mode](#msw-interceptors) integrates with `onfetch` by transmitting requests to the `fetch()` method of an object and passing this object to `onfetch` as the \"context\". By doing so `onfetch` can intercept the requests.\n\nYou can write a custom context like:\n\n```js\nclass SimpleContext {\n  fetch = async () =\u003e new Response('original');\n}\n\nconst context = new SimpleContext();\n```\n\nThen use `onfetch.adopt()` to let `onfetch` use this `context`.\n\n```js\nawait onfetch.adopt(context);\n```\n\nNow, all the accesses to the `context`'s `fetch()` method get intercepted.\n\n```js\nonfetch('/basic').reply('mocked');\nonfetch('/bypass').reply(passThrough);\n\n// Logs 'mocked'\ncontext.fetch('/basic')\n  .then((res) =\u003e res.text())\n  .then(console.log);\n\n// Logs 'original'\ncontext.fetch('/bypass')\n  .then((res) =\u003e res.text())\n  .then(console.log);\n```\n\n## Request Flow\n\nIn default mode:\n\n![onfetch captures the tests' fetch requests, makes real requests on demand, redirects the redirect-responses, and then sends back the mocked responses.](./docs/default-flow.svg)\n\nIn [service worker mode](#service-worker):\n\n![Service worker captures the client tests' all requests, and sends back to client onfetch. Client onfetch may make real request, and will send back the mocked response via service worker. Browser will auto redirect the redirect-responses by sending new requests to service worker.](./docs/sw-flow.svg)\n\n## Options\n\nConfigurable via `onfetch.config`.\n\n### Default Rule\n\nThe rule used when all `onfetch` rules failed to match a request. You can form a rule by constructing a `InterceptRule` object, which accepts the same params as `onfetch`.\n\n```js\nimport onfetch, { InterceptRule } from 'onfetch';\nonfetch.config({\n  defaultRule: new InterceptRule('').reply('default'),\n});\n```\n\nDefaults to:\n\n```js\nnew InterceptRule('').reply((req) =\u003e {\n  throw new Error(`No onfetch rule matches this request to '${req.url}'`);\n})\n```\n\n### Abort Error\n\nConstructor for abort errors. It should extend from `Error` and its instance should have the `name` property set to `AbortError`.\n\n```js\nimport onfetch from 'onfetch';\nonfetch.config({\n  AbortError: PutItHere,\n});\n```\n\nIn Browsers, defaults to:\n\n```js\nDOMException.bind(null, 'The user aborted a request.', 'AbortError');\n```\n\nIn Node, defaults to:\n\n```js\nclass AbortError extends Error {\n  name = 'AbortError';\n\n  constructor() {\n    super('The user aborted a request.');\n  }\n}\n```\n\n### Bypass Redirect\n\nSet this to `true` to bypass `onfetch` [redirection](#redirect).\n\n```js\nimport onfetch from 'onfetch';\nonfetch.config({\n  bypassRedirect: true, // or false\n});\n```\n\nIn [advanced modes](#auto-advanced), this defaults to `true`, as the browser / upstream package will handle the redirections on its own (see [request flow](#request-flow)). So we can overcome some [redirect limitations](#limitations).\n\n## Q\u0026A\n\nCheckout our [Q\u0026A Discussions][q-a] for your answers. 👍\n\n## Contributing\n\nCheckout our [Contributing Guide][contributing] please. 👍\n\n## License\n[license]: https://github.com/PaperStrike/onfetch/blob/main/LICENSE\n\n[ISC][license]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperstrike%2Fonfetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaperstrike%2Fonfetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperstrike%2Fonfetch/lists"}