The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .github
    ├── banner.svg
    └── workflows
    │   ├── autofix.yml
    │   └── ci.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.config.ts
├── eslint.config.mjs
├── examples
    ├── README.md
    ├── body.mjs
    ├── error-handling.mjs
    ├── first-request.mjs
    ├── headers.mjs
    ├── methods.mjs
    ├── proxy.mjs
    ├── query-string.mjs
    └── type-safety.ts
├── node.d.ts
├── package.json
├── playground
    ├── index.mjs
    └── index.ts
├── pnpm-lock.yaml
├── renovate.json
├── src
    ├── base.ts
    ├── error.ts
    ├── fetch.ts
    ├── index.ts
    ├── node.ts
    ├── types.ts
    └── utils.ts
├── test
    └── index.test.ts
├── tsconfig.json
└── vitest.config.ts


/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | end_of_line = lf
 5 | insert_final_newline = true
 6 | trim_trailing_whitespace = true
 7 | charset = utf-8
 8 | 
 9 | [*.js]
10 | indent_style = space
11 | indent_size = 2
12 | 
13 | [{package.json,*.yml,*.cjson}]
14 | indent_style = space
15 | indent_size = 2
16 | 


--------------------------------------------------------------------------------
/.github/workflows/autofix.yml:
--------------------------------------------------------------------------------
 1 | name: autofix.ci # needed to securely identify the workflow
 2 | 
 3 | on:
 4 |   pull_request:
 5 |   push:
 6 |     branches: ["main"]
 7 | 
 8 | permissions:
 9 |   contents: read
10 | 
11 | jobs:
12 |   autofix:
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - uses: actions/checkout@v4
16 |       - run: corepack enable
17 |       - uses: actions/setup-node@v4
18 |         with:
19 |           node-version: 22
20 |           cache: "pnpm"
21 |       - run: pnpm install
22 |       - name: Fix lint issues
23 |         run: pnpm run lint:fix
24 |       - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
25 |         with:
26 |           commit-message: "chore: apply automated updates"
27 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: ci
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |   pull_request:
 8 |     branches:
 9 |       - main
10 | 
11 | jobs:
12 |   ci:
13 |     runs-on: ubuntu-latest
14 |     strategy:
15 |       matrix:
16 |         node: [18, 20, 22]
17 |     steps:
18 |       - uses: actions/checkout@v4
19 |       - run: corepack enable
20 |       - uses: actions/setup-node@v4
21 |         with:
22 |           node-version: ${{ matrix.node }}
23 |           cache: "pnpm"
24 |       - run: pnpm install
25 |       - run: pnpm lint
26 |       - run: pnpm build
27 |       - run: pnpm vitest --coverage
28 |       - uses: codecov/codecov-action@v5
29 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules
3 | *.log
4 | .DS_Store
5 | coverage
6 | dist
7 | types
8 | .conf*
9 | 


--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 |   "trailingComma": "es5"
3 | }
4 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | # Changelog
  2 | 
  3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
  4 | 
  5 | ## v1.4.1
  6 | 
  7 | [compare changes](https://github.com/unjs/ofetch/compare/v1.4.0...v1.4.1)
  8 | 
  9 | ### 🩹 Fixes
 10 | 
 11 | - Remove undefined `method` and `query`/`params` from fetch options ([#451](https://github.com/unjs/ofetch/pull/451))
 12 | - Use `response._bodyInit` as fallback for react-native and qq ([#398](https://github.com/unjs/ofetch/pull/398))
 13 | 
 14 | ### 🏡 Chore
 15 | 
 16 | - **examples:** Fix typos ([#450](https://github.com/unjs/ofetch/pull/450))
 17 | - Update dependencies ([caaf04d](https://github.com/unjs/ofetch/commit/caaf04d))
 18 | - Update eslint config ([b4c9990](https://github.com/unjs/ofetch/commit/b4c9990))
 19 | 
 20 | ### ✅ Tests
 21 | 
 22 | - Fix typo ([#448](https://github.com/unjs/ofetch/pull/448))
 23 | 
 24 | ### ❤️ Contributors
 25 | 
 26 | - Joshua Sosso ([@joshmossas](http://github.com/joshmossas))
 27 | - Pooya Parsa ([@pi0](http://github.com/pi0))
 28 | - @beer ([@iiio2](http://github.com/iiio2))
 29 | - Cooper Roper <cooproper@hotmail.com>
 30 | 
 31 | ## v1.4.0
 32 | 
 33 | [compare changes](https://github.com/unjs/ofetch/compare/v1.3.4...v1.4.0)
 34 | 
 35 | ### 🚀 Enhancements
 36 | 
 37 | - Support `retryDelay` with callback function ([#372](https://github.com/unjs/ofetch/pull/372))
 38 | - Add better message and code for timeout error ([#351](https://github.com/unjs/ofetch/pull/351))
 39 | - Allow custom global options for `$fetch.create` ([#401](https://github.com/unjs/ofetch/pull/401))
 40 | - Support interceptors arrays ([#353](https://github.com/unjs/ofetch/pull/353))
 41 | - Always clone and normalize `options.headers` and `options.query` ([#436](https://github.com/unjs/ofetch/pull/436))
 42 | 
 43 | ### 🩹 Fixes
 44 | 
 45 | - Export types from `node` export condition ([#407](https://github.com/unjs/ofetch/pull/407))
 46 | - Use wrapper to allow patching global `fetch` ([#377](https://github.com/unjs/ofetch/pull/377))
 47 | 
 48 | ### 📖 Documentation
 49 | 
 50 | - Add docs for using undici dispatcher ([#389](https://github.com/unjs/ofetch/pull/389))
 51 | 
 52 | ### 🌊 Types
 53 | 
 54 | - Add `agent` and `dispatcher` options (node-specific) ([#308](https://github.com/unjs/ofetch/pull/308))
 55 | 
 56 | ### 🏡 Chore
 57 | 
 58 | - **release:** V1.3.4 ([5cc16a0](https://github.com/unjs/ofetch/commit/5cc16a0))
 59 | - Remove extra space ([#384](https://github.com/unjs/ofetch/pull/384))
 60 | - Update deps ([509a037](https://github.com/unjs/ofetch/commit/509a037))
 61 | - Update to eslint v9 ([e63c598](https://github.com/unjs/ofetch/commit/e63c598))
 62 | - Apply automated fixes ([f8f5413](https://github.com/unjs/ofetch/commit/f8f5413))
 63 | - Add back spoiler ([dba1915](https://github.com/unjs/ofetch/commit/dba1915))
 64 | - Add experimental for `Too Early` status ([#426](https://github.com/unjs/ofetch/pull/426))
 65 | - Update dependencies ([b5fe505](https://github.com/unjs/ofetch/commit/b5fe505))
 66 | - Update deps ([20f67b9](https://github.com/unjs/ofetch/commit/20f67b9))
 67 | 
 68 | ### ✅ Tests
 69 | 
 70 | - Add additional tests for hook errors ([7ff4d11](https://github.com/unjs/ofetch/commit/7ff4d11))
 71 | 
 72 | ### 🤖 CI
 73 | 
 74 | - Update node version ([4faac04](https://github.com/unjs/ofetch/commit/4faac04))
 75 | - Update autifix ([79483ab](https://github.com/unjs/ofetch/commit/79483ab))
 76 | 
 77 | ### ❤️ Contributors
 78 | 
 79 | - Pooya Parsa ([@pi0](http://github.com/pi0))
 80 | - Antoine Rey <antoinerey38@gmail.com>
 81 | - Cafu Chino <kirino@cafuchino.cn>
 82 | - Marco Solazzi <marco.solazzi@gmail.com>
 83 | - @beer ([@iiio2](http://github.com/iiio2))
 84 | - Daniel Roe ([@danielroe](http://github.com/danielroe))
 85 | - Arlo <webfansplz@gmail.com>
 86 | - Alexander Topalo <topaloalexander@gmail.com>
 87 | - Sam Blowes <samblowes@hotmail.com>
 88 | - Kongmoumou ([@kongmoumou](http://github.com/kongmoumou))
 89 | 
 90 | ## v1.3.4
 91 | 
 92 | [compare changes](https://github.com/unjs/ofetch/compare/v1.3.3...v1.3.4)
 93 | 
 94 | ### 🚀 Enhancements
 95 | 
 96 | - Export all types ([#280](https://github.com/unjs/ofetch/pull/280))
 97 | - Expose `GlobalOptions` type ([#307](https://github.com/unjs/ofetch/pull/307))
 98 | 
 99 | ### 🩹 Fixes
100 | 
101 | - Clear abort timeout after response was received ([#369](https://github.com/unjs/ofetch/pull/369))
102 | 
103 | ### 💅 Refactors
104 | 
105 | - Remove extra line ([#374](https://github.com/unjs/ofetch/pull/374))
106 | 
107 | ### 📖 Documentation
108 | 
109 | - Add initial examples ([#288](https://github.com/unjs/ofetch/pull/288))
110 | 
111 | ### 📦 Build
112 | 
113 | - Add top level `react-native` field ([03680dd](https://github.com/unjs/ofetch/commit/03680dd))
114 | 
115 | ### 🏡 Chore
116 | 
117 | - **release:** V1.3.3 ([31c61c1](https://github.com/unjs/ofetch/commit/31c61c1))
118 | - Update dependencies ([308f03f](https://github.com/unjs/ofetch/commit/308f03f))
119 | - Ignore conflicting ts error for now ([3a73165](https://github.com/unjs/ofetch/commit/3a73165))
120 | - Improve docs ([173d5b9](https://github.com/unjs/ofetch/commit/173d5b9))
121 | - Remove lagon ([#346](https://github.com/unjs/ofetch/pull/346))
122 | - Update lockfile ([4b6d1ba](https://github.com/unjs/ofetch/commit/4b6d1ba))
123 | - Fix build error ([472c4d9](https://github.com/unjs/ofetch/commit/472c4d9))
124 | - Update node-fetch-native ([fa2cc07](https://github.com/unjs/ofetch/commit/fa2cc07))
125 | 
126 | ### ❤️ Contributors
127 | 
128 | - Pooya Parsa ([@pi0](http://github.com/pi0))
129 | - Alex Liu ([@Mini-ghost](http://github.com/Mini-ghost))
130 | - Danila Rodichkin ([@daniluk4000](http://github.com/daniluk4000))
131 | - Maxime Pauvert ([@maximepvrt](http://github.com/maximepvrt))
132 | - Estéban ([@Barbapapazes](http://github.com/Barbapapazes))
133 | - Saman <bounoable@gmail.com>
134 | 
135 | ## v1.3.3
136 | 
137 | [compare changes](https://github.com/unjs/ofetch/compare/v1.3.2...v1.3.3)
138 | 
139 | ### 🩹 Fixes
140 | 
141 | - Augment `FetchError` type to include `IFetchError` ([#279](https://github.com/unjs/ofetch/pull/279))
142 | 
143 | ### ❤️ Contributors
144 | 
145 | - Johann Schopplich ([@johannschopplich](http://github.com/johannschopplich))
146 | 
147 | ## v1.3.2
148 | 
149 | [compare changes](https://github.com/unjs/ofetch/compare/v1.3.1...v1.3.2)
150 | 
151 | ### 🩹 Fixes
152 | 
153 | - Hide getters from console and pass `cause` ([905244a](https://github.com/unjs/ofetch/commit/905244a))
154 | 
155 | ### ❤️ Contributors
156 | 
157 | - Pooya Parsa ([@pi0](http://github.com/pi0))
158 | 
159 | ## v1.3.1
160 | 
161 | [compare changes](https://github.com/unjs/ofetch/compare/v1.3.0...v1.3.1)
162 | 
163 | ### 🏡 Chore
164 | 
165 | - Update dependencies ([c72976f](https://github.com/unjs/ofetch/commit/c72976f))
166 | 
167 | ### ❤️ Contributors
168 | 
169 | - Pooya Parsa ([@pi0](http://github.com/pi0))
170 | 
171 | ## v1.3.0
172 | 
173 | [compare changes](https://github.com/unjs/ofetch/compare/v1.2.1...v1.3.0)
174 | 
175 | ### 🚀 Enhancements
176 | 
177 | - Support customizable `retryStatusCodes` ([#109](https://github.com/unjs/ofetch/pull/109))
178 | - Add `options` field and improve formatting of errors ([#270](https://github.com/unjs/ofetch/pull/270))
179 | - Automatically enable duplex to stream request body ([#275](https://github.com/unjs/ofetch/pull/275))
180 | 
181 | ### 🩹 Fixes
182 | 
183 | - Avoid binding `.native` to `$fetch` ([#272](https://github.com/unjs/ofetch/pull/272))
184 | - Skip reading body with `204` responses and `HEAD` requests ([#171](https://github.com/unjs/ofetch/pull/171), [#84](https://github.com/unjs/ofetch/pull/84))
185 | - Improve response body check for node 16 compatibility ([64d3aed](https://github.com/unjs/ofetch/commit/64d3aed))
186 | - Avoid serializing buffer body ([#273](https://github.com/unjs/ofetch/pull/273))
187 | - Move body handling out of request block ([15a28fb](https://github.com/unjs/ofetch/commit/15a28fb))
188 | 
189 | ### 💅 Refactors
190 | 
191 | - Remove unused `response?: boolean` option ([#223](https://github.com/unjs/ofetch/pull/223))
192 | - Pass all fetch context to the error ([b70e6b0](https://github.com/unjs/ofetch/commit/b70e6b0))
193 | - **error:** Factory pattern for getters ([6139785](https://github.com/unjs/ofetch/commit/6139785))
194 | 
195 | ### 📖 Documentation
196 | 
197 | - Improve explanation about `body` option ([#276](https://github.com/unjs/ofetch/pull/276))
198 | 
199 | ### 🏡 Chore
200 | 
201 | - **release:** V1.2.1 ([bb98cb5](https://github.com/unjs/ofetch/commit/bb98cb5))
202 | - Remove accidental `raw` response type addition ([8589cae](https://github.com/unjs/ofetch/commit/8589cae))
203 | 
204 | ### ❤️ Contributors
205 | 
206 | - Pooya Parsa ([@pi0](http://github.com/pi0))
207 | - Nozomu Ikuta 
208 | - Daniil Bezuglov
209 | 
210 | ## v1.2.1
211 | 
212 | [compare changes](https://github.com/unjs/ofetch/compare/v1.2.0...v1.2.1)
213 | 
214 | ### 📦 Build
215 | 
216 | - Add missing `node` export condition ([4081170](https://github.com/unjs/ofetch/commit/4081170))
217 | 
218 | ### 🏡 Chore
219 | 
220 | - Update dependencies ([d18584d](https://github.com/unjs/ofetch/commit/d18584d))
221 | 
222 | ### ✅ Tests
223 | 
224 | - Speedup with background close ([567fb35](https://github.com/unjs/ofetch/commit/567fb35))
225 | 
226 | ### ❤️ Contributors
227 | 
228 | - Pooya Parsa ([@pi0](http://github.com/pi0))
229 | 
230 | ## v1.2.0
231 | 
232 | [compare changes](https://github.com/unjs/ofetch/compare/v1.1.1...v1.2.0)
233 | 
234 | ### 🚀 Enhancements
235 | 
236 | - Support `retryDelay` ([#262](https://github.com/unjs/ofetch/pull/262))
237 | - Support `timeout` and `AbortController` ([#268](https://github.com/unjs/ofetch/pull/268))
238 | 
239 | ### 🩹 Fixes
240 | 
241 | - Always uppercase `method` option ([#259](https://github.com/unjs/ofetch/pull/259))
242 | - **pkg:** Fix ts type resolution for `/node` subpath ([#256](https://github.com/unjs/ofetch/pull/256))
243 | - Make all `createFetch` options optional ([#266](https://github.com/unjs/ofetch/pull/266))
244 | 
245 | ### 📖 Documentation
246 | 
247 | - Clarify retry behavior ([#264](https://github.com/unjs/ofetch/pull/264))
248 | - Fix typo ([de66aad](https://github.com/unjs/ofetch/commit/de66aad))
249 | 
250 | ### 🏡 Chore
251 | 
252 | - Update dev dependencies ([8fc7d96](https://github.com/unjs/ofetch/commit/8fc7d96))
253 | - **release:** V1.1.1 ([41c3b56](https://github.com/unjs/ofetch/commit/41c3b56))
254 | - Update dependencies ([db2434c](https://github.com/unjs/ofetch/commit/db2434c))
255 | - Add autofix ci ([a953a33](https://github.com/unjs/ofetch/commit/a953a33))
256 | - Apply automated fixes ([bbdfb9c](https://github.com/unjs/ofetch/commit/bbdfb9c))
257 | 
258 | ### ✅ Tests
259 | 
260 | - Update tests ([db2ad50](https://github.com/unjs/ofetch/commit/db2ad50))
261 | 
262 | ### 🎨 Styles
263 | 
264 | - Lint code ([b3c6a96](https://github.com/unjs/ofetch/commit/b3c6a96))
265 | - Lint repo with prettier ([2be558c](https://github.com/unjs/ofetch/commit/2be558c))
266 | 
267 | ### ❤️ Contributors
268 | 
269 | - Daniil Bezuglov 
270 | - Pooya Parsa ([@pi0](http://github.com/pi0))
271 | - Sébastien Chopin ([@Atinux](http://github.com/Atinux))
272 | - Tmk ([@tmkx](http://github.com/tmkx))
273 | - Murisceman <murisceman@gmail.com>
274 | - Heb ([@Hebilicious](http://github.com/Hebilicious))
275 | 
276 | ## v1.1.1
277 | 
278 | [compare changes](https://github.com/unjs/ofetch/compare/v1.1.0...v1.1.1)
279 | 
280 | 
281 | ### 🏡 Chore
282 | 
283 |   - Update dev dependencies ([8fc7d96](https://github.com/unjs/ofetch/commit/8fc7d96))
284 | 
285 | ### ❤️  Contributors
286 | 
287 | - Pooya Parsa ([@pi0](http://github.com/pi0))
288 | 
289 | ## v1.1.0
290 | 
291 | [compare changes](https://github.com/unjs/ofetch/compare/v1.0.1...v1.1.0)
292 | 
293 | 
294 | ### 🚀 Enhancements
295 | 
296 |   - Support `ignoreResponseError` option ([#221](https://github.com/unjs/ofetch/pull/221))
297 |   - **pkg:** Add export conditions for runtime keys ([#246](https://github.com/unjs/ofetch/pull/246))
298 | 
299 | ### 🩹 Fixes
300 | 
301 |   - Pass empty object to headers initializer to prevent crash on chrome 49 ([#235](https://github.com/unjs/ofetch/pull/235))
302 |   - Export `ResponseMap` type to allow composition of `ofetch` ([#232](https://github.com/unjs/ofetch/pull/232))
303 |   - Fix issues with native node fetch ([#245](https://github.com/unjs/ofetch/pull/245))
304 |   - **pkg:** Add `./package.json` subpath ([253707a](https://github.com/unjs/ofetch/commit/253707a))
305 |   - Deep merge fetch options ([#243](https://github.com/unjs/ofetch/pull/243))
306 | 
307 | ### 📖 Documentation
308 | 
309 |   - **readme:** Use `_data` rather than `data` for raw requests ([#239](https://github.com/unjs/ofetch/pull/239))
310 |   - Mention `DELETE` is no-retry be default ([#241](https://github.com/unjs/ofetch/pull/241))
311 | 
312 | ### 🏡 Chore
313 | 
314 |   - **readme:** Small improvements ([65921a1](https://github.com/unjs/ofetch/commit/65921a1))
315 | 
316 | ### 🤖 CI
317 | 
318 |   - Enable tests against node `16`, `18` and `20` ([351fc80](https://github.com/unjs/ofetch/commit/351fc80))
319 | 
320 | ### ❤️  Contributors
321 | 
322 | - Dennis Meuwissen 
323 | - Pooya Parsa ([@pi0](http://github.com/pi0))
324 | - Alex Korytskyi ([@alex-key](http://github.com/alex-key))
325 | - Arunanshu Biswas 
326 | - Jonathan Bakebwa <codebender828@gmail.com>
327 | - Ilya Semenov ([@IlyaSemenov](http://github.com/IlyaSemenov))
328 | - _lmmmmmm <lmmmmmm12138@gmail.com>
329 | - Jonas Thelemann ([@dargmuesli](http://github.com/dargmuesli))
330 | - Sébastien Chopin <seb@nuxtjs.com>
331 | 
332 | ## v1.0.1
333 | 
334 | [compare changes](https://github.com/unjs/ofetch/compare/v1.0.0...v1.0.1)
335 | 
336 | 
337 | ### 🩹 Fixes
338 | 
339 |   - Improve error message for request errors ([#199](https://github.com/unjs/ofetch/pull/199))
340 | 
341 | ### 📖 Documentation
342 | 
343 |   - Fix small typos ([#200](https://github.com/unjs/ofetch/pull/200))
344 |   - Fix typo ([#175](https://github.com/unjs/ofetch/pull/175))
345 |   - Add agent option usage ([#173](https://github.com/unjs/ofetch/pull/173))
346 |   - Add note about http agent ([#202](https://github.com/unjs/ofetch/pull/202))
347 | 
348 | ### 📦 Build
349 | 
350 |   - Use standalone commonjs dist ([#211](https://github.com/unjs/ofetch/pull/211))
351 | 
352 | ### 🏡 Chore
353 | 
354 |   - Update lockfile ([67a7fa4](https://github.com/unjs/ofetch/commit/67a7fa4))
355 |   - Remove build badge ([9a878b6](https://github.com/unjs/ofetch/commit/9a878b6))
356 |   - Update ufo ([3776210](https://github.com/unjs/ofetch/commit/3776210))
357 |   - Update release script ([50a58ab](https://github.com/unjs/ofetch/commit/50a58ab))
358 | 
359 | ### 🎨 Styles
360 | 
361 |   - Format with prettier ([aabfb9a](https://github.com/unjs/ofetch/commit/aabfb9a))
362 | 
363 | ### ❤️  Contributors
364 | 
365 | - Pooya Parsa <pooya@pi0.io>
366 | - Daniel West <daniel@silverback.is>
367 | - Sébastien Chopin <seb@nuxtjs.com>
368 | - Nozomu Ikuta 
369 | - Yuyin 
370 | - Kricsleo 
371 | - 0xflotus <0xflotus@gmail.com>
372 | 
373 | ## [1.0.0](https://github.com/unjs/ofetch/compare/v0.4.21...v1.0.0) (2022-11-15)
374 | 
375 | 
376 | ### ⚠ BREAKING CHANGES
377 | 
378 | * drop undici support
379 | 
380 | ### Features
381 | 
382 | * expose `$fetch.native` ([ff697d7](https://github.com/unjs/ofetch/commit/ff697d7b4a43c4399897b69097485b3785dfd661))
383 | * expose `ofetch` named export ([d8fc46f](https://github.com/unjs/ofetch/commit/d8fc46f21a51f0aac75118905fb999a62d46c793))
384 | 
385 | 
386 | * drop undici support ([c7d8c93](https://github.com/unjs/ofetch/commit/c7d8c93b1dc9af6f556b713d63787d4295709908))
387 | 
388 | ### [0.4.21](https://github.com/unjs/ofetch/compare/v0.4.20...v0.4.21) (2022-11-03)
389 | 
390 | 
391 | ### Features
392 | 
393 | * add `status` and `statusText` to fetch errors ([#152](https://github.com/unjs/ofetch/issues/152)) ([784a7c0](https://github.com/unjs/ofetch/commit/784a7c0524a60406b0ba09055502107ef57ef5c9))
394 | 
395 | 
396 | ### Bug Fixes
397 | 
398 | * only call error handler if status code is >= 400 ([#153](https://github.com/unjs/ofetch/issues/153)) ([385f7fe](https://github.com/unjs/ofetch/commit/385f7fe9c92d1ee614919c0ce3a9586acf031d05))
399 | 
400 | ### [0.4.20](https://github.com/unjs/ofetch/compare/v0.4.19...v0.4.20) (2022-10-17)
401 | 
402 | 
403 | ### Bug Fixes
404 | 
405 | * add backwards-compatible subpath declarations ([#144](https://github.com/unjs/ofetch/issues/144)) ([3a48c21](https://github.com/unjs/ofetch/commit/3a48c21a0a5deb41be7ca8e9ea176c49e6d6ba56))
406 | * **types:** allow synchronous interceptors to be passed ([#128](https://github.com/unjs/ofetch/issues/128)) ([46e8f3c](https://github.com/unjs/ofetch/commit/46e8f3c70bbae09b123db42ab868631cdf9a45af))
407 | 
408 | ### [0.4.19](https://github.com/unjs/ofetch/compare/v0.4.18...v0.4.19) (2022-09-19)
409 | 
410 | 
411 | ### Features
412 | 
413 | * support `responseType: 'stream'` as `ReadableStream` ([#100](https://github.com/unjs/ofetch/issues/100)) ([5a19f73](https://github.com/unjs/ofetch/commit/5a19f73e97b13b6679ee2c75ad10b87480599d9b))
414 | 
415 | 
416 | ### Bug Fixes
417 | 
418 | * do not retry when fetch is aborted ([#112](https://github.com/unjs/ofetch/issues/112)) ([b73fe67](https://github.com/unjs/ofetch/commit/b73fe67aaa7d5d5b1a283b653e7cd7fb30a4cc21))
419 | 
420 | ### [0.4.18](https://github.com/unjs/ofetch/compare/v0.4.17...v0.4.18) (2022-05-20)
421 | 
422 | 
423 | ### Bug Fixes
424 | 
425 | * only serialize JSON bodies ([#80](https://github.com/unjs/ofetch/issues/80)) ([dc237d4](https://github.com/unjs/ofetch/commit/dc237d489a68936996c9ab95dce35ca9e0d2b2e4))
426 | 
427 | ### [0.4.17](https://github.com/unjs/ofetch/compare/v0.4.16...v0.4.17) (2022-05-11)
428 | 
429 | 
430 | ### Features
431 | 
432 | * use `node-fetch-native` ([a881acb](https://github.com/unjs/ofetch/commit/a881acb4067406092e25a1f97ff576040e279bce))
433 | 
434 | ### [0.4.16](https://github.com/unjs/ofetch/compare/v0.4.15...v0.4.16) (2022-04-29)
435 | 
436 | 
437 | ### Bug Fixes
438 | 
439 | * generalise `application/json` content types ([#75](https://github.com/unjs/ofetch/issues/75)) ([adaa03b](https://github.com/unjs/ofetch/commit/adaa03b56bf17df2a9a69eb471e84460e0d1ad12))
440 | 
441 | ### [0.4.15](https://github.com/unjs/ofetch/compare/v0.4.14...v0.4.15) (2022-01-18)
442 | 
443 | 
444 | ### Bug Fixes
445 | 
446 | * use `_data` rather than `data` to store deserialized response ([#49](https://github.com/unjs/ofetch/issues/49)) ([babb331](https://github.com/unjs/ofetch/commit/babb331c3c2dc52d682f87b3c2c660d23cf056b3))
447 | 
448 | ### [0.4.14](https://github.com/unjs/ofetch/compare/v0.4.13...v0.4.14) (2021-12-22)
449 | 
450 | 
451 | ### Bug Fixes
452 | 
453 | * avoid calling `fetch` with `globalOptions` context ([8ea2d2b](https://github.com/unjs/ofetch/commit/8ea2d2b5f9dcda333de5824862a7377085fd8bb4))
454 | 
455 | ### [0.4.13](https://github.com/unjs/ofetch/compare/v0.4.12...v0.4.13) (2021-12-21)
456 | 
457 | 
458 | ### Features
459 | 
460 | * `$fetch.create` support ([d7fb8f6](https://github.com/unjs/ofetch/commit/d7fb8f688c2b3f574430b40f7139ee144850be23))
461 | * initial interceptor support (resolves [#19](https://github.com/unjs/ofetch/issues/19)) ([1bf2dd9](https://github.com/unjs/ofetch/commit/1bf2dd928935b79b13a4e7a8fbd93365b082835f))
462 | 
463 | ### [0.4.12](https://github.com/unjs/ofetch/compare/v0.4.11...v0.4.12) (2021-12-21)
464 | 
465 | 
466 | ### Bug Fixes
467 | 
468 | * avoid overriding headers ([4b74e45](https://github.com/unjs/ofetch/commit/4b74e45f9989a993e725e7fe4d2e098442e457f1)), closes [#40](https://github.com/unjs/ofetch/issues/40) [#41](https://github.com/unjs/ofetch/issues/41)
469 | * only retry on known response codes (resolves [#31](https://github.com/unjs/ofetch/issues/31)) ([f7fff24](https://github.com/unjs/ofetch/commit/f7fff24acfde76029051fe26a88f993518a95735))
470 | 
471 | ### [0.4.11](https://github.com/unjs/ofetch/compare/v0.4.10...v0.4.11) (2021-12-17)
472 | 
473 | 
474 | ### Features
475 | 
476 | * return blob if `content-type` isn't text, svg, xml or json ([#39](https://github.com/unjs/ofetch/issues/39)) ([1029b9e](https://github.com/unjs/ofetch/commit/1029b9e2c982991d21d77ea33036d7c20a4536bb))
477 | 
478 | ### [0.4.10](https://github.com/unjs/ofetch/compare/v0.4.9...v0.4.10) (2021-12-14)
479 | 
480 | 
481 | ### Bug Fixes
482 | 
483 | * avoid optional chaining ([931d12d](https://github.com/unjs/ofetch/commit/931d12d945d5bc014b34f46d93a627dbb4eda3a7))
484 | 
485 | ### [0.4.9](https://github.com/unjs/ofetch/compare/v0.4.8...v0.4.9) (2021-12-14)
486 | 
487 | 
488 | ### Features
489 | 
490 | * improve json body handling ([4adb3bc](https://github.com/unjs/ofetch/commit/4adb3bc38d9423507ccdcd895b62a3a7d95f4144)), closes [#36](https://github.com/unjs/ofetch/issues/36)
491 | 
492 | ### [0.4.8](https://github.com/unjs/ofetch/compare/v0.4.7...v0.4.8) (2021-11-22)
493 | 
494 | 
495 | ### Bug Fixes
496 | 
497 | * add accept header when using json payload ([#30](https://github.com/unjs/ofetch/issues/30)) ([662145f](https://github.com/unjs/ofetch/commit/662145f8b74a18ba7d07e8eb6f3fd1af91941a22))
498 | 
499 | ### [0.4.7](https://github.com/unjs/ofetch/compare/v0.4.6...v0.4.7) (2021-11-18)
500 | 
501 | 
502 | ### Bug Fixes
503 | 
504 | * use `application/json` for array body  ([#29](https://github.com/unjs/ofetch/issues/29)) ([e794b1e](https://github.com/unjs/ofetch/commit/e794b1e4644f803e9c18c8813c432b29c7f9ef33))
505 | 
506 | ### [0.4.6](https://github.com/unjs/ofetch/compare/v0.4.5...v0.4.6) (2021-11-10)
507 | 
508 | 
509 | ### Bug Fixes
510 | 
511 | * add check for using `Error.captureStackTrace` ([#27](https://github.com/unjs/ofetch/issues/27)) ([0c55e1e](https://github.com/unjs/ofetch/commit/0c55e1ec1bf21fcb3a7fc6ed570956b4dfba80d5))
512 | * remove baseurl append on retry ([#25](https://github.com/unjs/ofetch/issues/25)) ([7e1b54d](https://github.com/unjs/ofetch/commit/7e1b54d1363ba3f5fe57fe8089babab921a8e9ea))
513 | 
514 | ### [0.4.5](https://github.com/unjs/ofetch/compare/v0.4.4...v0.4.5) (2021-11-05)
515 | 
516 | 
517 | ### Bug Fixes
518 | 
519 | * improve error handling for non-user errors ([6b965a5](https://github.com/unjs/ofetch/commit/6b965a5206bd3eb64b86d550dbba2932495bf67d))
520 | 
521 | ### [0.4.4](https://github.com/unjs/ofetch/compare/v0.4.3...v0.4.4) (2021-11-04)
522 | 
523 | 
524 | ### Bug Fixes
525 | 
526 | * allow `retry: false` ([ce8e4d3](https://github.com/unjs/ofetch/commit/ce8e4d31332403937bf7db0b45ecd54bb97c319f))
527 | 
528 | ### [0.4.3](https://github.com/unjs/ofetch/compare/v0.4.2...v0.4.3) (2021-11-04)
529 | 
530 | 
531 | ### Features
532 | 
533 | * experimental undici support ([dfa0b55](https://github.com/unjs/ofetch/commit/dfa0b554c72f1fe03bf3dc3cb0f47b7d306edb63))
534 | * **node:** pick `globalThis.fetch` when available over `node-fetch` ([54b779b](https://github.com/unjs/ofetch/commit/54b779b97a6722542bdfe4b0f6dd9e82e59a7010))
535 | * **node:** support http agent with `keepAlive` ([#22](https://github.com/unjs/ofetch/issues/22)) ([18a952a](https://github.com/unjs/ofetch/commit/18a952af10eb40823086837f71a921221e49c559))
536 | * support retry and default to `1` ([ec83366](https://github.com/unjs/ofetch/commit/ec83366ae3a0cbd2b2b093b92b99ef7fbb561ceb))
537 | 
538 | 
539 | ### Bug Fixes
540 | 
541 | * remove `at raw` from stack ([82351a8](https://github.com/unjs/ofetch/commit/82351a8b6f5fea0b062bff76881bd8b740352ca8))
542 | 
543 | ### [0.4.2](https://github.com/unjs/ofetch/compare/v0.4.1...v0.4.2) (2021-10-22)
544 | 
545 | 
546 | ### Features
547 | 
548 | * **cjs:** provide `fetch` and `$fetch.raw` exports ([529af1c](https://github.com/unjs/ofetch/commit/529af1c22b31b71ffe9bb21a1f13997ae7aac195))
549 | 
550 | ### [0.4.1](https://github.com/unjs/ofetch/compare/v0.4.0...v0.4.1) (2021-10-22)
551 | 
552 | 
553 | ### Bug Fixes
554 | 
555 | * avoid optional chaining for sake of webpack4 ([38a75fe](https://github.com/unjs/ofetch/commit/38a75fe599a2b96d2d5fe12f2e4630ae4f17a102))
556 | 
557 | ## [0.4.0](https://github.com/unjs/ofetch/compare/v0.3.2...v0.4.0) (2021-10-22)
558 | 
559 | 
560 | ### ⚠ BREAKING CHANGES
561 | 
562 | * upgrade to node-fetch 3.x
563 | 
564 | ### Features
565 | 
566 | * upgrade to node-fetch 3.x ([ec51edf](https://github.com/unjs/ofetch/commit/ec51edf1fd53e4ab4ef99ec3253ea95353abb50e))
567 | 
568 | ### [0.3.2](https://github.com/unjs/ofetch/compare/v0.3.1...v0.3.2) (2021-10-22)
569 | 
570 | 
571 | ### Features
572 | 
573 | * allow for custom response parser with `parseResponse` ([#16](https://github.com/unjs/ofetch/issues/16)) ([463ced6](https://github.com/unjs/ofetch/commit/463ced66c4d12f8d380d0bca2c5ff2febf38af7e))
574 | 
575 | 
576 | ### Bug Fixes
577 | 
578 | * check for `globalThis` before fallback to shims ([#20](https://github.com/unjs/ofetch/issues/20)) ([b5c0c3b](https://github.com/unjs/ofetch/commit/b5c0c3bb38289a83164570ffa7458c3f47c6d41b))
579 | 
580 | ### [0.3.1](https://github.com/unjs/ofetch/compare/v0.3.0...v0.3.1) (2021-08-26)
581 | 
582 | 
583 | ### Bug Fixes
584 | 
585 | * include typings ([#12](https://github.com/unjs/ofetch/issues/12)) ([2d9a9e9](https://github.com/unjs/ofetch/commit/2d9a9e921ab42756f6420b728bc5f47447d59df3))
586 | 
587 | ## [0.3.0](https://github.com/unjs/ofetch/compare/v0.2.0...v0.3.0) (2021-08-25)
588 | 
589 | 
590 | ### ⚠ BREAKING CHANGES
591 | 
592 | * use export condition to automatically use node-fetch
593 | 
594 | ### Features
595 | 
596 | * direct export fetch implementation ([65b27dd](https://github.com/unjs/ofetch/commit/65b27ddb863790af8637b9da1c50c8fba14a295d))
597 | * use export condition to automatically use node-fetch ([b81082b](https://github.com/unjs/ofetch/commit/b81082b6ab1b8e89fa620699c4f9206101230805))
598 | 
599 | ## [0.2.0](https://github.com/unjs/ofetch/compare/v0.1.8...v0.2.0) (2021-04-06)
600 | 
601 | 
602 | ### ⚠ BREAKING CHANGES
603 | 
604 | * don't inline dependencies
605 | 
606 | ### Features
607 | 
608 | * don't inline dependencies ([cf3578b](https://github.com/unjs/ofetch/commit/cf3578baf265e1044f22c4ba42b227831c6fd183))
609 | 
610 | ### [0.1.8](https://github.com/unjs/ofetch/compare/v0.1.7...v0.1.8) (2021-02-22)
611 | 
612 | 
613 | ### Bug Fixes
614 | 
615 | * **pkg:** add top level node.d.ts ([dcc1358](https://github.com/unjs/ofetch/commit/dcc13582747ba8404dd26b48d3db755b7775b78b))
616 | 
617 | ### [0.1.7](https://github.com/unjs/ofetch/compare/v0.1.6...v0.1.7) (2021-02-19)
618 | 
619 | 
620 | ### Features
621 | 
622 | * support automatic json body for post requests ([#7](https://github.com/unjs/ofetch/issues/7)) ([97d0987](https://github.com/unjs/ofetch/commit/97d0987131e006e72aac6d1d4acb063f3e53953d))
623 | 
624 | ### [0.1.6](https://github.com/unjs/ofetch/compare/v0.1.5...v0.1.6) (2021-01-12)
625 | 
626 | ### [0.1.5](https://github.com/unjs/ofetch/compare/v0.1.4...v0.1.5) (2021-01-04)
627 | 
628 | 
629 | ### Bug Fixes
630 | 
631 | * **pkg:** use same export names for incompatible tools ([7fc450a](https://github.com/unjs/ofetch/commit/7fc450ac81596de1dea53380dc9ef3ae8ceb2304))
632 | 
633 | ### [0.1.4](https://github.com/unjs/ofetch/compare/v0.1.3...v0.1.4) (2020-12-16)
634 | 
635 | 
636 | ### Features
637 | 
638 | * update ufo to 0.5 (reducing bundle size) ([837707d](https://github.com/unjs/ofetch/commit/837707d2ed03a7c6e69127849bf0c25ae182982d))
639 | 
640 | ### [0.1.3](https://github.com/unjs/ofetch/compare/v0.1.2...v0.1.3) (2020-12-16)
641 | 
642 | 
643 | ### Bug Fixes
644 | 
645 | * ufo 0.3.1 ([e56e73e](https://github.com/unjs/ofetch/commit/e56e73e90bb6ad9be88f7c8413053744a64c702e))
646 | 
647 | ### [0.1.2](https://github.com/unjs/ofetch/compare/v0.1.1...v0.1.2) (2020-12-16)
648 | 
649 | 
650 | ### Bug Fixes
651 | 
652 | * update ufo to 0.3 ([52d84e7](https://github.com/unjs/ofetch/commit/52d84e75034c3c6fd7542b2829e06f6d87f069c2))
653 | 
654 | ### [0.1.1](https://github.com/unjs/ofetch/compare/v0.0.7...v0.1.1) (2020-12-12)
655 | 
656 | 
657 | ### Bug Fixes
658 | 
659 | * preserve params when using baseURL ([c3a63e2](https://github.com/unjs/ofetch/commit/c3a63e2b337b09b082eb9faf8e23e818d866c49c))
660 | 
661 | ### [0.0.7](https://github.com/unjs/ofetch/compare/v0.0.6...v0.0.7) (2020-12-12)
662 | 
663 | ### [0.0.6](https://github.com/unjs/ofetch/compare/v0.0.5...v0.0.6) (2020-12-12)
664 | 
665 | 
666 | ### Bug Fixes
667 | 
668 | * **pkg:** fix top level named exports ([0b51462](https://github.com/unjs/ofetch/commit/0b514620dcfa65d156397114b87ed5e4f28e33a1))
669 | 
670 | ### [0.0.5](https://github.com/unjs/ofetch/compare/v0.0.4...v0.0.5) (2020-12-12)
671 | 
672 | 
673 | ### Bug Fixes
674 | 
675 | * **pkg:** fix ./node in exports ([c6b27b7](https://github.com/unjs/ofetch/commit/c6b27b7cb61d66444f3d43bfa5226057ec7a9c95))
676 | 
677 | ### [0.0.4](https://github.com/unjs/ofetch/compare/v0.0.3...v0.0.4) (2020-12-12)
678 | 
679 | 
680 | ### Features
681 | 
682 | * support params ([e6a56ff](https://github.com/unjs/ofetch/commit/e6a56ff083244fac918e29058aaf28bf87c98384))
683 | 
684 | ### [0.0.3](https://github.com/unjs/ofetch/compare/v0.0.2...v0.0.3) (2020-12-12)
685 | 
686 | 
687 | ### Features
688 | 
689 | * bundle ufo and destr for easier bundler integration ([8f5ba88](https://github.com/unjs/ofetch/commit/8f5ba88f1ac0aa40ff2c99316da98a71d6dcc7e8))
690 | * support `$fetch.raw` and improve docs ([f9f70a5](https://github.com/unjs/ofetch/commit/f9f70a59222bc0d0166cbe9a03eebf2a73682398))
691 | 
692 | ### [0.0.2](https://github.com/unjs/ofetch/compare/v0.0.1...v0.0.2) (2020-12-09)
693 | 
694 | 
695 | ### Bug Fixes
696 | 
697 | * **pkg:** add top level dist ([6da17ca](https://github.com/unjs/ofetch/commit/6da17cad07e08cff9e5ea9e8b505638d560bcb47))
698 | 
699 | ### 0.0.1 (2020-12-09)
700 | 
701 | 
702 | ### Features
703 | 
704 | * universal + isomorphic builds ([a873702](https://github.com/unjs/ofetch/commit/a873702c336c7ecce87c506d81c146db9f7516d0))
705 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) Pooya Parsa <pooya@pi0.io>
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # ofetch
  2 | 
  3 | [![npm version][npm-version-src]][npm-version-href]
  4 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
  5 | [![bundle][bundle-src]][bundle-href]
  6 | [![Codecov][codecov-src]][codecov-href]
  7 | [![License][license-src]][license-href]
  8 | [![JSDocs][jsdocs-src]][jsdocs-href]
  9 | 
 10 | A better fetch API. Works on node, browser, and workers.
 11 | 
 12 | <details>
 13 |   <summary>Spoiler</summary>
 14 |   <img src="https://media.giphy.com/media/Dn1QRA9hqMcoMz9zVZ/giphy.gif">
 15 | </details>
 16 | 
 17 | ## 🚀 Quick Start
 18 | 
 19 | Install:
 20 | 
 21 | ```bash
 22 | # npm
 23 | npm i ofetch
 24 | 
 25 | # yarn
 26 | yarn add ofetch
 27 | ```
 28 | 
 29 | Import:
 30 | 
 31 | ```js
 32 | // ESM / Typescript
 33 | import { ofetch } from "ofetch";
 34 | 
 35 | // CommonJS
 36 | const { ofetch } = require("ofetch");
 37 | ```
 38 | 
 39 | ## ✔️ Works with Node.js
 40 | 
 41 | We use [conditional exports](https://nodejs.org/api/packages.html#packages_conditional_exports) to detect Node.js
 42 | and automatically use [unjs/node-fetch-native](https://github.com/unjs/node-fetch-native). If `globalThis.fetch` is available, will be used instead. To leverage Node.js 17.5.0 experimental native fetch API use [`--experimental-fetch` flag](https://nodejs.org/dist/latest-v17.x/docs/api/cli.html#--experimental-fetch).
 43 | 
 44 | ## ✔️ Parsing Response
 45 | 
 46 | `ofetch` will smartly parse JSON and native values using [destr](https://github.com/unjs/destr), falling back to the text if it fails to parse.
 47 | 
 48 | ```js
 49 | const { users } = await ofetch("/api/users");
 50 | ```
 51 | 
 52 | For binary content types, `ofetch` will instead return a `Blob` object.
 53 | 
 54 | You can optionally provide a different parser than `destr`, or specify `blob`, `arrayBuffer`, or `text` to force parsing the body with the respective `FetchResponse` method.
 55 | 
 56 | ```js
 57 | // Use JSON.parse
 58 | await ofetch("/movie?lang=en", { parseResponse: JSON.parse });
 59 | 
 60 | // Return text as is
 61 | await ofetch("/movie?lang=en", { parseResponse: (txt) => txt });
 62 | 
 63 | // Get the blob version of the response
 64 | await ofetch("/api/generate-image", { responseType: "blob" });
 65 | ```
 66 | 
 67 | ## ✔️ JSON Body
 68 | 
 69 | If an object or a class with a `.toJSON()` method is passed to the `body` option, `ofetch` automatically stringifies it.
 70 | 
 71 | `ofetch` utilizes `JSON.stringify()` to convert the passed object. Classes without a `.toJSON()` method have to be converted into a string value in advance before being passed to the `body` option.
 72 | 
 73 | For `PUT`, `PATCH`, and `POST` request methods, when a string or object body is set, `ofetch` adds the default `content-type: "application/json"` and `accept: "application/json"` headers (which you can always override).
 74 | 
 75 | Additionally, `ofetch` supports binary responses with `Buffer`, `ReadableStream`, `Stream`, and [compatible body types](https://developer.mozilla.org/en-US/docs/Web/API/fetch#body). `ofetch` will automatically set the `duplex: "half"` option for streaming support!
 76 | 
 77 | **Example:**
 78 | 
 79 | ```js
 80 | const { users } = await ofetch("/api/users", {
 81 |   method: "POST",
 82 |   body: { some: "json" },
 83 | });
 84 | ```
 85 | 
 86 | ## ✔️ Handling Errors
 87 | 
 88 | `ofetch` Automatically throws errors when `response.ok` is `false` with a friendly error message and compact stack (hiding internals).
 89 | 
 90 | A parsed error body is available with `error.data`. You may also use `FetchError` type.
 91 | 
 92 | ```ts
 93 | await ofetch("https://google.com/404");
 94 | // FetchError: [GET] "https://google/404": 404 Not Found
 95 | //     at async main (/project/playground.ts:4:3)
 96 | ```
 97 | 
 98 | To catch error response:
 99 | 
100 | ```ts
101 | await ofetch("/url").catch((error) => error.data);
102 | ```
103 | 
104 | To bypass status error catching you can set `ignoreResponseError` option:
105 | 
106 | ```ts
107 | await ofetch("/url", { ignoreResponseError: true });
108 | ```
109 | 
110 | ## ✔️ Auto Retry
111 | 
112 | `ofetch` Automatically retries the request if an error happens and if the response status code is included in `retryStatusCodes` list:
113 | 
114 | **Retry status codes:**
115 | 
116 | - `408` - Request Timeout
117 | - `409` - Conflict
118 | - `425` - Too Early ([Experimental](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Early-Data))
119 | - `429` - Too Many Requests
120 | - `500` - Internal Server Error
121 | - `502` - Bad Gateway
122 | - `503` - Service Unavailable
123 | - `504` - Gateway Timeout
124 | 
125 | You can specify the amount of retry and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option.
126 | 
127 | The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. If you set a custom value for `retry` it will **always retry** for all requests.
128 | 
129 | The default for `retryDelay` is `0` ms.
130 | 
131 | ```ts
132 | await ofetch("http://google.com/404", {
133 |   retry: 3,
134 |   retryDelay: 500, // ms
135 |   retryStatusCodes: [ 404, 500 ], // response status codes to retry
136 | });
137 | ```
138 | 
139 | ## ✔️ Timeout
140 | 
141 | You can specify `timeout` in milliseconds to automatically abort a request after a timeout (default is disabled).
142 | 
143 | ```ts
144 | await ofetch("http://google.com/404", {
145 |   timeout: 3000, // Timeout after 3 seconds
146 | });
147 | ```
148 | 
149 | ## ✔️ Type Friendly
150 | 
151 | The response can be type assisted:
152 | 
153 | ```ts
154 | const article = await ofetch<Article>(`/api/article/${id}`);
155 | // Auto complete working with article.id
156 | ```
157 | 
158 | ## ✔️ Adding `baseURL`
159 | 
160 | By using `baseURL` option, `ofetch` prepends it for trailing/leading slashes and query search params for baseURL using [ufo](https://github.com/unjs/ufo):
161 | 
162 | ```js
163 | await ofetch("/config", { baseURL });
164 | ```
165 | 
166 | ## ✔️ Adding Query Search Params
167 | 
168 | By using `query` option (or `params` as alias), `ofetch` adds query search params to the URL by preserving the query in the request itself using [ufo](https://github.com/unjs/ufo):
169 | 
170 | ```js
171 | await ofetch("/movie?lang=en", { query: { id: 123 } });
172 | ```
173 | 
174 | ## ✔️ Interceptors
175 | 
176 | Providing async interceptors to hook into lifecycle events of `ofetch` call is possible.
177 | 
178 | You might want to use `ofetch.create` to set shared interceptors.
179 | 
180 | ### `onRequest({ request, options })`
181 | 
182 | `onRequest` is called as soon as `ofetch` is called, allowing you to modify options or do simple logging.
183 | 
184 | ```js
185 | await ofetch("/api", {
186 |   async onRequest({ request, options }) {
187 |     // Log request
188 |     console.log("[fetch request]", request, options);
189 | 
190 |     // Add `?t=1640125211170` to query search params
191 |     options.query = options.query || {};
192 |     options.query.t = new Date();
193 |   },
194 | });
195 | ```
196 | 
197 | ### `onRequestError({ request, options, error })`
198 | 
199 | `onRequestError` will be called when the fetch request fails.
200 | 
201 | ```js
202 | await ofetch("/api", {
203 |   async onRequestError({ request, options, error }) {
204 |     // Log error
205 |     console.log("[fetch request error]", request, error);
206 |   },
207 | });
208 | ```
209 | 
210 | ### `onResponse({ request, options, response })`
211 | 
212 | `onResponse` will be called after `fetch` call and parsing body.
213 | 
214 | ```js
215 | await ofetch("/api", {
216 |   async onResponse({ request, response, options }) {
217 |     // Log response
218 |     console.log("[fetch response]", request, response.status, response.body);
219 |   },
220 | });
221 | ```
222 | 
223 | ### `onResponseError({ request, options, response })`
224 | 
225 | `onResponseError` is the same as `onResponse` but will be called when fetch happens but `response.ok` is not `true`.
226 | 
227 | ```js
228 | await ofetch("/api", {
229 |   async onResponseError({ request, response, options }) {
230 |     // Log error
231 |     console.log(
232 |       "[fetch response error]",
233 |       request,
234 |       response.status,
235 |       response.body
236 |     );
237 |   },
238 | });
239 | ```
240 | 
241 | ### Passing array of interceptors
242 | 
243 | If necessary, it's also possible to pass an array of function that will be called sequentially.
244 | 
245 | ```js
246 | await ofetch("/api", {
247 |   onRequest: [
248 |     () => {
249 |       /* Do something */
250 |     },
251 |     () => {
252 |       /* Do something else */
253 |     },
254 |   ],
255 | });
256 | ```
257 | 
258 | ## ✔️ Create fetch with default options
259 | 
260 | This utility is useful if you need to use common options across several fetch calls.
261 | 
262 | **Note:** Defaults will be cloned at one level and inherited. Be careful about nested options like `headers`.
263 | 
264 | ```js
265 | const apiFetch = ofetch.create({ baseURL: "/api" });
266 | 
267 | apiFetch("/test"); // Same as ofetch('/test', { baseURL: '/api' })
268 | ```
269 | 
270 | ## 💡 Adding headers
271 | 
272 | By using `headers` option, `ofetch` adds extra headers in addition to the request default headers:
273 | 
274 | ```js
275 | await ofetch("/movies", {
276 |   headers: {
277 |     Accept: "application/json",
278 |     "Cache-Control": "no-cache",
279 |   },
280 | });
281 | ```
282 | 
283 | ## 🍣 Access to Raw Response
284 | 
285 | If you need to access raw response (for headers, etc), you can use `ofetch.raw`:
286 | 
287 | ```js
288 | const response = await ofetch.raw("/sushi");
289 | 
290 | // response._data
291 | // response.headers
292 | // ...
293 | ```
294 | 
295 | ## 🌿 Using Native Fetch
296 | 
297 | As a shortcut, you can use `ofetch.native` that provides native `fetch` API
298 | 
299 | ```js
300 | const json = await ofetch.native("/sushi").then((r) => r.json());
301 | ```
302 | 
303 | ## 🕵️ Adding HTTP(S) Agent
304 | 
305 | In Node.js (>= 18) environments, you can provide a custom dispatcher to intercept requests and support features such as Proxy and self-signed certificates. This feature is enabled by [undici](https://undici.nodejs.org/) built-in Node.js. [read more](https://undici.nodejs.org/#/docs/api/Dispatcher) about the Dispatcher API.
306 | 
307 | Some available agents:
308 | 
309 | - `ProxyAgent`: A Proxy Agent class that implements the Agent API. It allows the connection through a proxy in a simple way. ([docs](https://undici.nodejs.org/#/docs/api/ProxyAgent))
310 | - `MockAgent`: A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. ([docs](https://undici.nodejs.org/#/docs/api/MockAgent))
311 | - `Agent`: Agent allows dispatching requests against multiple different origins. ([docs](https://undici.nodejs.org/#/docs/api/Agent))
312 | 
313 | **Example:** Set a proxy agent for one request:
314 | 
315 | ```ts
316 | import { ProxyAgent } from "undici";
317 | import { ofetch } from "ofetch";
318 | 
319 | const proxyAgent = new ProxyAgent("http://localhost:3128");
320 | const data = await ofetch("https://icanhazip.com", { dispatcher: proxyAgent });
321 | ```
322 | 
323 | **Example:** Create a custom fetch instance that has proxy enabled:
324 | 
325 | ```ts
326 | import { ProxyAgent, setGlobalDispatcher } from "undici";
327 | import { ofetch } from "ofetch";
328 | 
329 | const proxyAgent = new ProxyAgent("http://localhost:3128");
330 | const fetchWithProxy = ofetch.create({ dispatcher: proxyAgent });
331 | 
332 | const data = await fetchWithProxy("https://icanhazip.com");
333 | ```
334 | 
335 | **Example:** Set a proxy agent for all requests:
336 | 
337 | ```ts
338 | import { ProxyAgent, setGlobalDispatcher } from "undici";
339 | import { ofetch } from "ofetch";
340 | 
341 | const proxyAgent = new ProxyAgent("http://localhost:3128");
342 | setGlobalDispatcher(proxyAgent);
343 | 
344 | const data = await ofetch("https://icanhazip.com");
345 | ```
346 | 
347 | **Example:** Allow self-signed certificates (USE AT YOUR OWN RISK!)
348 | 
349 | ```ts
350 | import { ProxyAgent } from "undici";
351 | import { ofetch } from "ofetch";
352 | 
353 | // Note: This makes fetch unsecure against MITM attacks. USE AT YOUR OWN RISK!
354 | const unsecureProxyAgent = new ProxyAgent({ requestTls: { rejectUnauthorized: false } });
355 | const unsecureFetch = ofetch.create({ dispatcher: unsecureProxyAgent });
356 | 
357 | const data = await unsecureFetch("https://www.squid-cache.org/");
358 | ```
359 | 
360 | On older Node.js version (<18), you might also use use `agent`:
361 | 
362 | ```ts
363 | import { HttpsProxyAgent } from "https-proxy-agent";
364 | 
365 | await ofetch("/api", {
366 |   agent: new HttpsProxyAgent("http://example.com"),
367 | });
368 | ```
369 | 
370 | ### `keepAlive` support (only works for Node < 18)
371 | 
372 | By setting the `FETCH_KEEP_ALIVE` environment variable to `true`, an HTTP/HTTPS agent will be registered that keeps sockets around even when there are no outstanding requests, so they can be used for future requests without having to re-establish a TCP connection.
373 | 
374 | **Note:** This option can potentially introduce memory leaks. Please check [node-fetch/node-fetch#1325](https://github.com/node-fetch/node-fetch/pull/1325).
375 | 
376 | ## 📦 Bundler Notes
377 | 
378 | - All targets are exported with Module and CommonJS format and named exports
379 | - No export is transpiled for the sake of modern syntax
380 |   - You probably need to transpile `ofetch`, `destr`, and `ufo` packages with Babel for ES5 support
381 | - You need to polyfill `fetch` global for supporting legacy browsers like using [unfetch](https://github.com/developit/unfetch)
382 | 
383 | ## ❓ FAQ
384 | 
385 | **Why export is called `ofetch` instead of `fetch`?**
386 | 
387 | Using the same name of `fetch` can be confusing since API is different but still, it is a fetch so using the closest possible alternative. You can, however, import `{ fetch }` from `ofetch` which is auto-polyfill for Node.js and using native otherwise.
388 | 
389 | **Why not have default export?**
390 | 
391 | Default exports are always risky to be mixed with CommonJS exports.
392 | 
393 | This also guarantees we can introduce more utils without breaking the package and also encourage using `ofetch` name.
394 | 
395 | **Why not transpiled?**
396 | 
397 | By transpiling libraries, we push the web backward with legacy code which is unneeded for most of the users.
398 | 
399 | If you need to support legacy users, you can optionally transpile the library in your build pipeline.
400 | 
401 | ## License
402 | 
403 | MIT. Made with 💖
404 | 
405 | <!-- Badges -->
406 | 
407 | [npm-version-src]: https://img.shields.io/npm/v/ofetch?style=flat&colorA=18181B&colorB=F0DB4F
408 | [npm-version-href]: https://npmjs.com/package/ofetch
409 | [npm-downloads-src]: https://img.shields.io/npm/dm/ofetch?style=flat&colorA=18181B&colorB=F0DB4F
410 | [npm-downloads-href]: https://npmjs.com/package/ofetch
411 | [codecov-src]: https://img.shields.io/codecov/c/gh/unjs/ofetch/main?style=flat&colorA=18181B&colorB=F0DB4F
412 | [codecov-href]: https://codecov.io/gh/unjs/ofetch
413 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/ofetch?style=flat&colorA=18181B&colorB=F0DB4F
414 | [bundle-href]: https://bundlephobia.com/result?p=ofetch
415 | [license-src]: https://img.shields.io/github/license/unjs/ofetch.svg?style=flat&colorA=18181B&colorB=F0DB4F
416 | [license-href]: https://github.com/unjs/ofetch/blob/main/LICENSE
417 | [jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F
418 | [jsdocs-href]: https://www.jsdocs.io/package/ofetch
419 | 


--------------------------------------------------------------------------------
/build.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineBuildConfig } from "unbuild";
 2 | 
 3 | export default defineBuildConfig({
 4 |   declaration: true,
 5 |   rollup: {
 6 |     emitCJS: true,
 7 |   },
 8 |   entries: ["src/index", "src/node"],
 9 |   externals: ["undici"],
10 | });
11 | 


--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
 1 | import unjs from "eslint-config-unjs";
 2 | 
 3 | // https://github.com/unjs/eslint-config
 4 | export default unjs({
 5 |   ignores: [],
 6 |   rules: {
 7 |     "no-undef": 0,
 8 |     "unicorn/consistent-destructuring": 0,
 9 |     "unicorn/no-await-expression-member": 0,
10 |     "@typescript-eslint/no-empty-object-type": 0,
11 |   },
12 | });
13 | 


--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # ofetch examples
2 | 
3 | In this directory you can find some examples of how to use ofetch.
4 | 
5 | <!-- To learn more, you can read the [ofetch first hand tutorial on unjs.io](https://unjs.io/resources/learn/ofetch-101-first-hand). -->
6 | 


--------------------------------------------------------------------------------
/examples/body.mjs:
--------------------------------------------------------------------------------
 1 | import { ofetch } from "ofetch";
 2 | 
 3 | const response = await ofetch("https://api.github.com/markdown", {
 4 |   method: "POST",
 5 |   // To provide a body, we need to use the `body` option and just use an object.
 6 |   body: {
 7 |     text: "UnJS is **awesome**!\n\nCheck out their [website](https://unjs.io).",
 8 |   },
 9 | });
10 | 
11 | console.log(response);
12 | 


--------------------------------------------------------------------------------
/examples/error-handling.mjs:
--------------------------------------------------------------------------------
 1 | import { ofetch } from "ofetch";
 2 | 
 3 | try {
 4 |   await ofetch("https://api.github.com", {
 5 |     method: "POST",
 6 |   });
 7 | } catch (error) {
 8 |   // Error will be pretty printed
 9 |   console.error(error);
10 | 
11 |   // This allows us to access the error body
12 |   console.log(error.data);
13 | }
14 | 


--------------------------------------------------------------------------------
/examples/first-request.mjs:
--------------------------------------------------------------------------------
1 | import { ofetch } from "ofetch";
2 | 
3 | const data = await ofetch("https://ungh.cc/repos/unjs/ofetch");
4 | 
5 | console.log(data);
6 | 


--------------------------------------------------------------------------------
/examples/headers.mjs:
--------------------------------------------------------------------------------
 1 | import { ofetch } from "ofetch";
 2 | 
 3 | const response = await ofetch("https://api.github.com/gists", {
 4 |   method: "POST",
 5 |   headers: {
 6 |     Authorization: `token ${process.env.GH_TOKEN}`,
 7 |   },
 8 |   body: {
 9 |     description: "This is a gist created by ofetch.",
10 |     public: true,
11 |     files: {
12 |       "unjs.txt": {
13 |         content: "UnJS is awesome!",
14 |       },
15 |     },
16 |   },
17 | }); // Be careful, we use the GitHub API directly.
18 | 
19 | console.log(response.url);
20 | 


--------------------------------------------------------------------------------
/examples/methods.mjs:
--------------------------------------------------------------------------------
1 | import { ofetch } from "ofetch";
2 | 
3 | const response = await ofetch("https://api.github.com/gists", {
4 |   method: "POST",
5 | }); // Be careful, we use the GitHub API directly.
6 | 
7 | console.log(response);
8 | 


--------------------------------------------------------------------------------
/examples/proxy.mjs:
--------------------------------------------------------------------------------
 1 | import { Agent } from "undici";
 2 | import { ofetch } from "ofetch";
 3 | 
 4 | // Note: This makes fetch unsecure to MITM attacks. USE AT YOUR OWN RISK!
 5 | const unsecureAgent = new Agent({ connect: { rejectUnauthorized: false } });
 6 | const unsecureFetch = ofetch.create({ dispatcher: unsecureAgent });
 7 | const data = await unsecureFetch("https://www.squid-cache.org/");
 8 | 
 9 | console.log(data);
10 | 


--------------------------------------------------------------------------------
/examples/query-string.mjs:
--------------------------------------------------------------------------------
 1 | import { ofetch } from "ofetch";
 2 | 
 3 | const response = await ofetch("https://api.github.com/repos/unjs/ofetch/tags", {
 4 |   query: {
 5 |     per_page: 2,
 6 |   },
 7 | }); // Be careful, we use the GitHub API directly.
 8 | 
 9 | console.log(response);
10 | 


--------------------------------------------------------------------------------
/examples/type-safety.ts:
--------------------------------------------------------------------------------
 1 | // @ts-ignore
 2 | import { ofetch } from "ofetch";
 3 | 
 4 | interface Repo {
 5 |   id: number;
 6 |   name: string;
 7 |   repo: string;
 8 |   description: string;
 9 |   stars: number;
10 | }
11 | 
12 | async function main() {
13 |   const { repo } = await ofetch<{ repo: Repo }>(
14 |     "https://ungh.cc/repos/unjs/ofetch"
15 |   );
16 | 
17 |   console.log(`The repo ${repo.name} has ${repo.stars} stars.`); // The repo object is now strongly typed.
18 | }
19 | 
20 | // eslint-disable-next-line unicorn/prefer-top-level-await
21 | main().catch(console.error);
22 | 


--------------------------------------------------------------------------------
/node.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./dist/node";
2 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "ofetch",
 3 |   "version": "1.4.1",
 4 |   "description": "A better fetch API. Works on node, browser and workers.",
 5 |   "repository": "unjs/ofetch",
 6 |   "license": "MIT",
 7 |   "sideEffects": false,
 8 |   "type": "module",
 9 |   "exports": {
10 |     "./package.json": "./package.json",
11 |     ".": {
12 |       "browser": "./dist/index.mjs",
13 |       "bun": "./dist/index.mjs",
14 |       "deno": "./dist/index.mjs",
15 |       "edge-light": "./dist/index.mjs",
16 |       "edge-routine": "./dist/index.mjs",
17 |       "netlify": "./dist/index.mjs",
18 |       "react-native": "./dist/index.mjs",
19 |       "wintercg": "./dist/index.mjs",
20 |       "worker": "./dist/index.mjs",
21 |       "workerd": "./dist/index.mjs",
22 |       "node": {
23 |         "import": {
24 |           "types": "./dist/node.d.mts",
25 |           "default": "./dist/node.mjs"
26 |         },
27 |         "require": {
28 |           "types": "./dist/node.d.cts",
29 |           "default": "./dist/node.cjs"
30 |         }
31 |       },
32 |       "import": {
33 |         "types": "./dist/index.d.mts",
34 |         "default": "./dist/index.mjs"
35 |       },
36 |       "require": {
37 |         "types": "./dist/node.d.cts",
38 |         "default": "./dist/node.cjs"
39 |       },
40 |       "types": "./dist/index.d.mts",
41 |       "default": "./dist/index.mjs"
42 |     },
43 |     "./node": {
44 |       "import": {
45 |         "types": "./dist/node.d.mts",
46 |         "default": "./dist/node.mjs"
47 |       },
48 |       "require": {
49 |         "types": "./dist/node.d.cts",
50 |         "default": "./dist/node.cjs"
51 |       }
52 |     }
53 |   },
54 |   "main": "./dist/node.cjs",
55 |   "module": "./dist/index.mjs",
56 |   "react-native": "./dist/index.mjs",
57 |   "types": "./dist/index.d.ts",
58 |   "files": [
59 |     "dist",
60 |     "node.d.ts"
61 |   ],
62 |   "scripts": {
63 |     "build": "unbuild",
64 |     "dev": "vitest",
65 |     "lint": "eslint . && prettier -c src test playground examples",
66 |     "lint:fix": "eslint --fix . && prettier -w src test playground examples",
67 |     "prepack": "pnpm build",
68 |     "play": "jiti playground/index.ts",
69 |     "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
70 |     "test": "pnpm lint && vitest run --coverage"
71 |   },
72 |   "dependencies": {
73 |     "destr": "^2.0.3",
74 |     "node-fetch-native": "^1.6.5",
75 |     "ufo": "^1.5.4"
76 |   },
77 |   "devDependencies": {
78 |     "@types/node": "^22.13.11",
79 |     "@vitest/coverage-v8": "^3.0.9",
80 |     "changelogen": "^0.6.1",
81 |     "eslint": "^9.23.0",
82 |     "eslint-config-unjs": "^0.4.2",
83 |     "fetch-blob": "^4.0.0",
84 |     "formdata-polyfill": "^4.0.10",
85 |     "h3": "^1.15.1",
86 |     "jiti": "^2.4.2",
87 |     "listhen": "^1.9.0",
88 |     "prettier": "^3.5.3",
89 |     "std-env": "^3.8.1",
90 |     "typescript": "^5.8.2",
91 |     "unbuild": "^3.5.0",
92 |     "undici": "^7.5.0",
93 |     "vitest": "^3.0.9"
94 |   },
95 |   "packageManager": "pnpm@9.15.9"
96 | }
97 | 


--------------------------------------------------------------------------------
/playground/index.mjs:
--------------------------------------------------------------------------------
 1 | import { $fetch } from "..";
 2 | 
 3 | async function main() {
 4 |   await $fetch("http://google.com/404");
 5 | }
 6 | 
 7 | // eslint-disable-next-line unicorn/prefer-top-level-await
 8 | main().catch((error) => {
 9 |   console.error(error);
10 |   // eslint-disable-next-line unicorn/no-process-exit
11 |   process.exit(1);
12 | });
13 | 


--------------------------------------------------------------------------------
/playground/index.ts:
--------------------------------------------------------------------------------
 1 | import { $fetch } from "../src/node";
 2 | 
 3 | async function main() {
 4 |   // const r = await $fetch<string>('http://google.com/404')
 5 |   const r = await $fetch<string>("http://httpstat.us/500");
 6 |   // const r = await $fetch<string>('http://httpstat/500')
 7 | 
 8 |   console.log(r);
 9 | }
10 | 
11 | // eslint-disable-next-line unicorn/prefer-top-level-await
12 | main().catch((error) => {
13 |   console.error(error);
14 |   // eslint-disable-next-line unicorn/no-process-exit
15 |   process.exit(1);
16 | });
17 | 


--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 |   "extends": ["github>unjs/renovate-config"]
3 | }
4 | 


--------------------------------------------------------------------------------
/src/base.ts:
--------------------------------------------------------------------------------
1 | export * from "./fetch";
2 | export * from "./error";
3 | 


--------------------------------------------------------------------------------
/src/error.ts:
--------------------------------------------------------------------------------
 1 | import type { FetchContext, IFetchError } from "./types";
 2 | 
 3 | // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
 4 | export class FetchError<T = any> extends Error implements IFetchError<T> {
 5 |   constructor(message: string, opts?: { cause: unknown }) {
 6 |     // @ts-ignore https://v8.dev/features/error-cause
 7 |     super(message, opts);
 8 | 
 9 |     this.name = "FetchError";
10 | 
11 |     // Polyfill cause for other runtimes
12 |     if (opts?.cause && !this.cause) {
13 |       this.cause = opts.cause;
14 |     }
15 |   }
16 | }
17 | 
18 | // Augment `FetchError` type to include `IFetchError` properties
19 | export interface FetchError<T = any> extends IFetchError<T> {}
20 | 
21 | export function createFetchError<T = any>(
22 |   ctx: FetchContext<T>
23 | ): IFetchError<T> {
24 |   const errorMessage = ctx.error?.message || ctx.error?.toString() || "";
25 | 
26 |   const method =
27 |     (ctx.request as Request)?.method || ctx.options?.method || "GET";
28 |   const url = (ctx.request as Request)?.url || String(ctx.request) || "/";
29 |   const requestStr = `[${method}] ${JSON.stringify(url)}`;
30 | 
31 |   const statusStr = ctx.response
32 |     ? `${ctx.response.status} ${ctx.response.statusText}`
33 |     : "<no response>";
34 | 
35 |   const message = `${requestStr}: ${statusStr}${
36 |     errorMessage ? ` ${errorMessage}` : ""
37 |   }`;
38 | 
39 |   const fetchError: FetchError<T> = new FetchError(
40 |     message,
41 |     ctx.error ? { cause: ctx.error } : undefined
42 |   );
43 | 
44 |   for (const key of ["request", "options", "response"] as const) {
45 |     Object.defineProperty(fetchError, key, {
46 |       get() {
47 |         return ctx[key];
48 |       },
49 |     });
50 |   }
51 | 
52 |   for (const [key, refKey] of [
53 |     ["data", "_data"],
54 |     ["status", "status"],
55 |     ["statusCode", "status"],
56 |     ["statusText", "statusText"],
57 |     ["statusMessage", "statusText"],
58 |   ] as const) {
59 |     Object.defineProperty(fetchError, key, {
60 |       get() {
61 |         return ctx.response && ctx.response[refKey];
62 |       },
63 |     });
64 |   }
65 | 
66 |   return fetchError;
67 | }
68 | 


--------------------------------------------------------------------------------
/src/fetch.ts:
--------------------------------------------------------------------------------
  1 | import type { Readable } from "node:stream";
  2 | import destr from "destr";
  3 | import { withBase, withQuery } from "ufo";
  4 | import { createFetchError } from "./error";
  5 | import {
  6 |   isPayloadMethod,
  7 |   isJSONSerializable,
  8 |   detectResponseType,
  9 |   resolveFetchOptions,
 10 |   callHooks,
 11 | } from "./utils";
 12 | import type {
 13 |   CreateFetchOptions,
 14 |   FetchResponse,
 15 |   ResponseType,
 16 |   FetchContext,
 17 |   $Fetch,
 18 |   FetchRequest,
 19 |   FetchOptions,
 20 | } from "./types";
 21 | 
 22 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
 23 | const retryStatusCodes = new Set([
 24 |   408, // Request Timeout
 25 |   409, // Conflict
 26 |   425, // Too Early (Experimental)
 27 |   429, // Too Many Requests
 28 |   500, // Internal Server Error
 29 |   502, // Bad Gateway
 30 |   503, // Service Unavailable
 31 |   504, // Gateway Timeout
 32 | ]);
 33 | 
 34 | // https://developer.mozilla.org/en-US/docs/Web/API/Response/body
 35 | const nullBodyResponses = new Set([101, 204, 205, 304]);
 36 | 
 37 | export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
 38 |   const {
 39 |     fetch = globalThis.fetch,
 40 |     Headers = globalThis.Headers,
 41 |     AbortController = globalThis.AbortController,
 42 |   } = globalOptions;
 43 | 
 44 |   async function onError(context: FetchContext): Promise<FetchResponse<any>> {
 45 |     // Is Abort
 46 |     // If it is an active abort, it will not retry automatically.
 47 |     // https://developer.mozilla.org/en-US/docs/Web/API/DOMException#error_names
 48 |     const isAbort =
 49 |       (context.error &&
 50 |         context.error.name === "AbortError" &&
 51 |         !context.options.timeout) ||
 52 |       false;
 53 |     // Retry
 54 |     if (context.options.retry !== false && !isAbort) {
 55 |       let retries;
 56 |       if (typeof context.options.retry === "number") {
 57 |         retries = context.options.retry;
 58 |       } else {
 59 |         retries = isPayloadMethod(context.options.method) ? 0 : 1;
 60 |       }
 61 | 
 62 |       const responseCode = (context.response && context.response.status) || 500;
 63 |       if (
 64 |         retries > 0 &&
 65 |         (Array.isArray(context.options.retryStatusCodes)
 66 |           ? context.options.retryStatusCodes.includes(responseCode)
 67 |           : retryStatusCodes.has(responseCode))
 68 |       ) {
 69 |         const retryDelay =
 70 |           typeof context.options.retryDelay === "function"
 71 |             ? context.options.retryDelay(context)
 72 |             : context.options.retryDelay || 0;
 73 |         if (retryDelay > 0) {
 74 |           await new Promise((resolve) => setTimeout(resolve, retryDelay));
 75 |         }
 76 |         // Timeout
 77 |         return $fetchRaw(context.request, {
 78 |           ...context.options,
 79 |           retry: retries - 1,
 80 |         });
 81 |       }
 82 |     }
 83 | 
 84 |     // Throw normalized error
 85 |     const error = createFetchError(context);
 86 | 
 87 |     // Only available on V8 based runtimes (https://v8.dev/docs/stack-trace-api)
 88 |     if (Error.captureStackTrace) {
 89 |       Error.captureStackTrace(error, $fetchRaw);
 90 |     }
 91 |     throw error;
 92 |   }
 93 | 
 94 |   const $fetchRaw: $Fetch["raw"] = async function $fetchRaw<
 95 |     T = any,
 96 |     R extends ResponseType = "json",
 97 |   >(_request: FetchRequest, _options: FetchOptions<R> = {}) {
 98 |     const context: FetchContext = {
 99 |       request: _request,
100 |       options: resolveFetchOptions<R, T>(
101 |         _request,
102 |         _options,
103 |         globalOptions.defaults as unknown as FetchOptions<R, T>,
104 |         Headers
105 |       ),
106 |       response: undefined,
107 |       error: undefined,
108 |     };
109 | 
110 |     // Uppercase method name
111 |     if (context.options.method) {
112 |       context.options.method = context.options.method.toUpperCase();
113 |     }
114 | 
115 |     if (context.options.onRequest) {
116 |       await callHooks(context, context.options.onRequest);
117 |     }
118 | 
119 |     if (typeof context.request === "string") {
120 |       if (context.options.baseURL) {
121 |         context.request = withBase(context.request, context.options.baseURL);
122 |       }
123 |       if (context.options.query) {
124 |         context.request = withQuery(context.request, context.options.query);
125 |         delete context.options.query;
126 |       }
127 |       if ("query" in context.options) {
128 |         delete context.options.query;
129 |       }
130 |       if ("params" in context.options) {
131 |         delete context.options.params;
132 |       }
133 |     }
134 | 
135 |     if (context.options.body && isPayloadMethod(context.options.method)) {
136 |       if (isJSONSerializable(context.options.body)) {
137 |         // JSON Body
138 |         // Automatically JSON stringify request bodies, when not already a string.
139 |         context.options.body =
140 |           typeof context.options.body === "string"
141 |             ? context.options.body
142 |             : JSON.stringify(context.options.body);
143 | 
144 |         // Set Content-Type and Accept headers to application/json by default
145 |         // for JSON serializable request bodies.
146 |         // Pass empty object as older browsers don't support undefined.
147 |         context.options.headers = new Headers(context.options.headers || {});
148 |         if (!context.options.headers.has("content-type")) {
149 |           context.options.headers.set("content-type", "application/json");
150 |         }
151 |         if (!context.options.headers.has("accept")) {
152 |           context.options.headers.set("accept", "application/json");
153 |         }
154 |       } else if (
155 |         // ReadableStream Body
156 |         ("pipeTo" in (context.options.body as ReadableStream) &&
157 |           typeof (context.options.body as ReadableStream).pipeTo ===
158 |             "function") ||
159 |         // Node.js Stream Body
160 |         typeof (context.options.body as Readable).pipe === "function"
161 |       ) {
162 |         // eslint-disable-next-line unicorn/no-lonely-if
163 |         if (!("duplex" in context.options)) {
164 |           context.options.duplex = "half";
165 |         }
166 |       }
167 |     }
168 | 
169 |     let abortTimeout: NodeJS.Timeout | undefined;
170 | 
171 |     // TODO: Can we merge signals?
172 |     if (!context.options.signal && context.options.timeout) {
173 |       const controller = new AbortController();
174 |       abortTimeout = setTimeout(() => {
175 |         const error = new Error(
176 |           "[TimeoutError]: The operation was aborted due to timeout"
177 |         );
178 |         error.name = "TimeoutError";
179 |         (error as any).code = 23; // DOMException.TIMEOUT_ERR
180 |         controller.abort(error);
181 |       }, context.options.timeout);
182 |       context.options.signal = controller.signal;
183 |     }
184 | 
185 |     try {
186 |       context.response = await fetch(
187 |         context.request,
188 |         context.options as RequestInit
189 |       );
190 |     } catch (error) {
191 |       context.error = error as Error;
192 |       if (context.options.onRequestError) {
193 |         await callHooks(
194 |           context as FetchContext & { error: Error },
195 |           context.options.onRequestError
196 |         );
197 |       }
198 |       return await onError(context);
199 |     } finally {
200 |       if (abortTimeout) {
201 |         clearTimeout(abortTimeout);
202 |       }
203 |     }
204 | 
205 |     const hasBody =
206 |       (context.response.body ||
207 |         // https://github.com/unjs/ofetch/issues/324
208 |         // https://github.com/unjs/ofetch/issues/294
209 |         // https://github.com/JakeChampion/fetch/issues/1454
210 |         (context.response as any)._bodyInit) &&
211 |       !nullBodyResponses.has(context.response.status) &&
212 |       context.options.method !== "HEAD";
213 |     if (hasBody) {
214 |       const responseType =
215 |         (context.options.parseResponse
216 |           ? "json"
217 |           : context.options.responseType) ||
218 |         detectResponseType(context.response.headers.get("content-type") || "");
219 | 
220 |       // We override the `.json()` method to parse the body more securely with `destr`
221 |       switch (responseType) {
222 |         case "json": {
223 |           const data = await context.response.text();
224 |           const parseFunction = context.options.parseResponse || destr;
225 |           context.response._data = parseFunction(data);
226 |           break;
227 |         }
228 |         case "stream": {
229 |           context.response._data =
230 |             context.response.body || (context.response as any)._bodyInit; // (see refs above)
231 |           break;
232 |         }
233 |         default: {
234 |           context.response._data = await context.response[responseType]();
235 |         }
236 |       }
237 |     }
238 | 
239 |     if (context.options.onResponse) {
240 |       await callHooks(
241 |         context as FetchContext & { response: FetchResponse<any> },
242 |         context.options.onResponse
243 |       );
244 |     }
245 | 
246 |     if (
247 |       !context.options.ignoreResponseError &&
248 |       context.response.status >= 400 &&
249 |       context.response.status < 600
250 |     ) {
251 |       if (context.options.onResponseError) {
252 |         await callHooks(
253 |           context as FetchContext & { response: FetchResponse<any> },
254 |           context.options.onResponseError
255 |         );
256 |       }
257 |       return await onError(context);
258 |     }
259 | 
260 |     return context.response;
261 |   };
262 | 
263 |   const $fetch = async function $fetch(request, options) {
264 |     const r = await $fetchRaw(request, options);
265 |     return r._data;
266 |   } as $Fetch;
267 | 
268 |   $fetch.raw = $fetchRaw;
269 | 
270 |   $fetch.native = (...args) => fetch(...args);
271 | 
272 |   $fetch.create = (defaultOptions = {}, customGlobalOptions = {}) =>
273 |     createFetch({
274 |       ...globalOptions,
275 |       ...customGlobalOptions,
276 |       defaults: {
277 |         ...globalOptions.defaults,
278 |         ...customGlobalOptions.defaults,
279 |         ...defaultOptions,
280 |       },
281 |     });
282 | 
283 |   return $fetch;
284 | }
285 | 


--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
 1 | import { createFetch } from "./base";
 2 | 
 3 | export * from "./base";
 4 | 
 5 | export type * from "./types";
 6 | 
 7 | // ref: https://github.com/tc39/proposal-global
 8 | const _globalThis = (function () {
 9 |   if (typeof globalThis !== "undefined") {
10 |     return globalThis;
11 |   }
12 |   /* eslint-disable unicorn/prefer-global-this */
13 |   if (typeof self !== "undefined") {
14 |     return self;
15 |   }
16 |   if (typeof window !== "undefined") {
17 |     return window;
18 |   }
19 |   if (typeof global !== "undefined") {
20 |     return global;
21 |   }
22 |   /* eslint-enable unicorn/prefer-global-this */
23 |   throw new Error("unable to locate global object");
24 | })();
25 | 
26 | // ref: https://github.com/unjs/ofetch/issues/295
27 | export const fetch = _globalThis.fetch
28 |   ? (...args: Parameters<typeof globalThis.fetch>) => _globalThis.fetch(...args)
29 |   : () => Promise.reject(new Error("[ofetch] global.fetch is not supported!"));
30 | 
31 | export const Headers = _globalThis.Headers;
32 | export const AbortController = _globalThis.AbortController;
33 | 
34 | export const ofetch = createFetch({ fetch, Headers, AbortController });
35 | export const $fetch = ofetch;
36 | 


--------------------------------------------------------------------------------
/src/node.ts:
--------------------------------------------------------------------------------
 1 | import http from "node:http";
 2 | import https, { AgentOptions } from "node:https";
 3 | import nodeFetch, {
 4 |   Headers as _Headers,
 5 |   AbortController as _AbortController,
 6 | } from "node-fetch-native";
 7 | 
 8 | import { createFetch } from "./base";
 9 | 
10 | export * from "./base";
11 | export type * from "./types";
12 | 
13 | export function createNodeFetch() {
14 |   const useKeepAlive = JSON.parse(process.env.FETCH_KEEP_ALIVE || "false");
15 |   if (!useKeepAlive) {
16 |     return nodeFetch;
17 |   }
18 | 
19 |   // https://github.com/node-fetch/node-fetch#custom-agent
20 |   const agentOptions: AgentOptions = { keepAlive: true };
21 |   const httpAgent = new http.Agent(agentOptions);
22 |   const httpsAgent = new https.Agent(agentOptions);
23 |   const nodeFetchOptions = {
24 |     agent(parsedURL: any) {
25 |       return parsedURL.protocol === "http:" ? httpAgent : httpsAgent;
26 |     },
27 |   };
28 | 
29 |   return function nodeFetchWithKeepAlive(
30 |     input: RequestInfo,
31 |     init?: RequestInit
32 |   ) {
33 |     return (nodeFetch as any)(input, { ...nodeFetchOptions, ...init });
34 |   };
35 | }
36 | 
37 | export const fetch = globalThis.fetch
38 |   ? (...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args)
39 |   : (createNodeFetch() as typeof globalThis.fetch);
40 | 
41 | export const Headers = globalThis.Headers || _Headers;
42 | export const AbortController = globalThis.AbortController || _AbortController;
43 | 
44 | export const ofetch = createFetch({ fetch, Headers, AbortController });
45 | export const $fetch = ofetch;
46 | 


--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
  1 | // --------------------------
  2 | // $fetch API
  3 | // --------------------------
  4 | 
  5 | export interface $Fetch {
  6 |   <T = any, R extends ResponseType = "json">(
  7 |     request: FetchRequest,
  8 |     options?: FetchOptions<R>
  9 |   ): Promise<MappedResponseType<R, T>>;
 10 |   raw<T = any, R extends ResponseType = "json">(
 11 |     request: FetchRequest,
 12 |     options?: FetchOptions<R>
 13 |   ): Promise<FetchResponse<MappedResponseType<R, T>>>;
 14 |   native: Fetch;
 15 |   create(defaults: FetchOptions, globalOptions?: CreateFetchOptions): $Fetch;
 16 | }
 17 | 
 18 | // --------------------------
 19 | // Options
 20 | // --------------------------
 21 | 
 22 | export interface FetchOptions<R extends ResponseType = ResponseType, T = any>
 23 |   extends Omit<RequestInit, "body">,
 24 |     FetchHooks<T, R> {
 25 |   baseURL?: string;
 26 | 
 27 |   body?: RequestInit["body"] | Record<string, any>;
 28 | 
 29 |   ignoreResponseError?: boolean;
 30 | 
 31 |   params?: Record<string, any>;
 32 | 
 33 |   query?: Record<string, any>;
 34 | 
 35 |   parseResponse?: (responseText: string) => any;
 36 | 
 37 |   responseType?: R;
 38 | 
 39 |   /**
 40 |    * @experimental Set to "half" to enable duplex streaming.
 41 |    * Will be automatically set to "half" when using a ReadableStream as body.
 42 |    * @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
 43 |    */
 44 |   duplex?: "half" | undefined;
 45 | 
 46 |   /**
 47 |    * Only supported in Node.js >= 18 using undici
 48 |    *
 49 |    * @see https://undici.nodejs.org/#/docs/api/Dispatcher
 50 |    */
 51 |   dispatcher?: InstanceType<typeof import("undici").Dispatcher>;
 52 | 
 53 |   /**
 54 |    * Only supported older Node.js versions using node-fetch-native polyfill.
 55 |    */
 56 |   agent?: unknown;
 57 | 
 58 |   /** timeout in milliseconds */
 59 |   timeout?: number;
 60 | 
 61 |   retry?: number | false;
 62 | 
 63 |   /** Delay between retries in milliseconds. */
 64 |   retryDelay?: number | ((context: FetchContext<T, R>) => number);
 65 | 
 66 |   /** Default is [408, 409, 425, 429, 500, 502, 503, 504] */
 67 |   retryStatusCodes?: number[];
 68 | }
 69 | 
 70 | export interface ResolvedFetchOptions<
 71 |   R extends ResponseType = ResponseType,
 72 |   T = any,
 73 | > extends FetchOptions<R, T> {
 74 |   headers: Headers;
 75 | }
 76 | 
 77 | export interface CreateFetchOptions {
 78 |   defaults?: FetchOptions;
 79 |   fetch?: Fetch;
 80 |   Headers?: typeof Headers;
 81 |   AbortController?: typeof AbortController;
 82 | }
 83 | 
 84 | export type GlobalOptions = Pick<
 85 |   FetchOptions,
 86 |   "timeout" | "retry" | "retryDelay"
 87 | >;
 88 | 
 89 | // --------------------------
 90 | // Hooks and Context
 91 | // --------------------------
 92 | 
 93 | export interface FetchContext<T = any, R extends ResponseType = ResponseType> {
 94 |   request: FetchRequest;
 95 |   options: ResolvedFetchOptions<R>;
 96 |   response?: FetchResponse<T>;
 97 |   error?: Error;
 98 | }
 99 | 
100 | type MaybePromise<T> = T | Promise<T>;
101 | type MaybeArray<T> = T | T[];
102 | 
103 | export type FetchHook<C extends FetchContext = FetchContext> = (
104 |   context: C
105 | ) => MaybePromise<void>;
106 | 
107 | export interface FetchHooks<T = any, R extends ResponseType = ResponseType> {
108 |   onRequest?: MaybeArray<FetchHook<FetchContext<T, R>>>;
109 |   onRequestError?: MaybeArray<FetchHook<FetchContext<T, R> & { error: Error }>>;
110 |   onResponse?: MaybeArray<
111 |     FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>
112 |   >;
113 |   onResponseError?: MaybeArray<
114 |     FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>
115 |   >;
116 | }
117 | 
118 | // --------------------------
119 | // Response Types
120 | // --------------------------
121 | 
122 | export interface ResponseMap {
123 |   blob: Blob;
124 |   text: string;
125 |   arrayBuffer: ArrayBuffer;
126 |   stream: ReadableStream<Uint8Array>;
127 | }
128 | 
129 | export type ResponseType = keyof ResponseMap | "json";
130 | 
131 | export type MappedResponseType<
132 |   R extends ResponseType,
133 |   JsonType = any,
134 | > = R extends keyof ResponseMap ? ResponseMap[R] : JsonType;
135 | 
136 | export interface FetchResponse<T> extends Response {
137 |   _data?: T;
138 | }
139 | 
140 | // --------------------------
141 | // Error
142 | // --------------------------
143 | 
144 | export interface IFetchError<T = any> extends Error {
145 |   request?: FetchRequest;
146 |   options?: FetchOptions;
147 |   response?: FetchResponse<T>;
148 |   data?: T;
149 |   status?: number;
150 |   statusText?: string;
151 |   statusCode?: number;
152 |   statusMessage?: string;
153 | }
154 | 
155 | // --------------------------
156 | // Other types
157 | // --------------------------
158 | 
159 | export type Fetch = typeof globalThis.fetch;
160 | 
161 | export type FetchRequest = RequestInfo;
162 | 
163 | export interface SearchParameters {
164 |   [key: string]: any;
165 | }
166 | 


--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
  1 | import type {
  2 |   FetchContext,
  3 |   FetchHook,
  4 |   FetchOptions,
  5 |   FetchRequest,
  6 |   ResolvedFetchOptions,
  7 |   ResponseType,
  8 | } from "./types";
  9 | 
 10 | const payloadMethods = new Set(
 11 |   Object.freeze(["PATCH", "POST", "PUT", "DELETE"])
 12 | );
 13 | export function isPayloadMethod(method = "GET") {
 14 |   return payloadMethods.has(method.toUpperCase());
 15 | }
 16 | 
 17 | export function isJSONSerializable(value: any) {
 18 |   if (value === undefined) {
 19 |     return false;
 20 |   }
 21 |   const t = typeof value;
 22 |   if (t === "string" || t === "number" || t === "boolean" || t === null) {
 23 |     return true;
 24 |   }
 25 |   if (t !== "object") {
 26 |     return false; // bigint, function, symbol, undefined
 27 |   }
 28 |   if (Array.isArray(value)) {
 29 |     return true;
 30 |   }
 31 |   if (value.buffer) {
 32 |     return false;
 33 |   }
 34 |   return (
 35 |     (value.constructor && value.constructor.name === "Object") ||
 36 |     typeof value.toJSON === "function"
 37 |   );
 38 | }
 39 | 
 40 | const textTypes = new Set([
 41 |   "image/svg",
 42 |   "application/xml",
 43 |   "application/xhtml",
 44 |   "application/html",
 45 | ]);
 46 | 
 47 | const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
 48 | 
 49 | // This provides reasonable defaults for the correct parser based on Content-Type header.
 50 | export function detectResponseType(_contentType = ""): ResponseType {
 51 |   if (!_contentType) {
 52 |     return "json";
 53 |   }
 54 | 
 55 |   // Value might look like: `application/json; charset=utf-8`
 56 |   const contentType = _contentType.split(";").shift() || "";
 57 | 
 58 |   if (JSON_RE.test(contentType)) {
 59 |     return "json";
 60 |   }
 61 | 
 62 |   // TODO
 63 |   // if (contentType === 'application/octet-stream') {
 64 |   //   return 'stream'
 65 |   // }
 66 | 
 67 |   if (textTypes.has(contentType) || contentType.startsWith("text/")) {
 68 |     return "text";
 69 |   }
 70 | 
 71 |   return "blob";
 72 | }
 73 | 
 74 | export function resolveFetchOptions<
 75 |   R extends ResponseType = ResponseType,
 76 |   T = any,
 77 | >(
 78 |   request: FetchRequest,
 79 |   input: FetchOptions<R, T> | undefined,
 80 |   defaults: FetchOptions<R, T> | undefined,
 81 |   Headers: typeof globalThis.Headers
 82 | ): ResolvedFetchOptions<R, T> {
 83 |   // Merge headers
 84 |   const headers = mergeHeaders(
 85 |     input?.headers ?? (request as Request)?.headers,
 86 |     defaults?.headers,
 87 |     Headers
 88 |   );
 89 | 
 90 |   // Merge query/params
 91 |   let query: Record<string, any> | undefined;
 92 |   if (defaults?.query || defaults?.params || input?.params || input?.query) {
 93 |     query = {
 94 |       ...defaults?.params,
 95 |       ...defaults?.query,
 96 |       ...input?.params,
 97 |       ...input?.query,
 98 |     };
 99 |   }
100 | 
101 |   return {
102 |     ...defaults,
103 |     ...input,
104 |     query,
105 |     params: query,
106 |     headers,
107 |   };
108 | }
109 | 
110 | function mergeHeaders(
111 |   input: HeadersInit | undefined,
112 |   defaults: HeadersInit | undefined,
113 |   Headers: typeof globalThis.Headers
114 | ): Headers {
115 |   if (!defaults) {
116 |     return new Headers(input);
117 |   }
118 |   const headers = new Headers(defaults);
119 |   if (input) {
120 |     for (const [key, value] of Symbol.iterator in input || Array.isArray(input)
121 |       ? input
122 |       : new Headers(input)) {
123 |       headers.set(key, value);
124 |     }
125 |   }
126 |   return headers;
127 | }
128 | 
129 | export async function callHooks<C extends FetchContext = FetchContext>(
130 |   context: C,
131 |   hooks: FetchHook<C> | FetchHook<C>[] | undefined
132 | ): Promise<void> {
133 |   if (hooks) {
134 |     if (Array.isArray(hooks)) {
135 |       for (const hook of hooks) {
136 |         await hook(context);
137 |       }
138 |     } else {
139 |       await hooks(context);
140 |     }
141 |   }
142 | }
143 | 


--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
  1 | import { Readable } from "node:stream";
  2 | import { listen } from "listhen";
  3 | import { getQuery, joinURL } from "ufo";
  4 | import {
  5 |   createApp,
  6 |   createError,
  7 |   eventHandler,
  8 |   readBody,
  9 |   readRawBody,
 10 |   toNodeListener,
 11 | } from "h3";
 12 | import {
 13 |   describe,
 14 |   beforeEach,
 15 |   beforeAll,
 16 |   afterAll,
 17 |   it,
 18 |   expect,
 19 |   vi,
 20 | } from "vitest";
 21 | import { Headers, FormData, Blob } from "node-fetch-native";
 22 | import { nodeMajorVersion } from "std-env";
 23 | import { $fetch } from "../src/node";
 24 | 
 25 | describe("ofetch", () => {
 26 |   let listener;
 27 |   const getURL = (url) => joinURL(listener.url, url);
 28 | 
 29 |   const fetch = vi.spyOn(globalThis, "fetch");
 30 | 
 31 |   beforeAll(async () => {
 32 |     const app = createApp()
 33 |       .use(
 34 |         "/ok",
 35 |         eventHandler(() => "ok")
 36 |       )
 37 |       .use(
 38 |         "/params",
 39 |         eventHandler((event) => getQuery(event.node.req.url || ""))
 40 |       )
 41 |       .use(
 42 |         "/url",
 43 |         eventHandler((event) => event.node.req.url)
 44 |       )
 45 |       .use(
 46 |         "/echo",
 47 |         eventHandler(async (event) => ({
 48 |           path: event.path,
 49 |           body:
 50 |             event.node.req.method === "POST"
 51 |               ? await readRawBody(event)
 52 |               : undefined,
 53 |           headers: event.node.req.headers,
 54 |         }))
 55 |       )
 56 |       .use(
 57 |         "/post",
 58 |         eventHandler(async (event) => ({
 59 |           body: await readBody(event),
 60 |           headers: event.node.req.headers,
 61 |         }))
 62 |       )
 63 |       .use(
 64 |         "/binary",
 65 |         eventHandler((event) => {
 66 |           event.node.res.setHeader("Content-Type", "application/octet-stream");
 67 |           return new Blob(["binary"]);
 68 |         })
 69 |       )
 70 |       .use(
 71 |         "/403",
 72 |         eventHandler(() =>
 73 |           createError({ status: 403, statusMessage: "Forbidden" })
 74 |         )
 75 |       )
 76 |       .use(
 77 |         "/408",
 78 |         eventHandler(() => createError({ status: 408 }))
 79 |       )
 80 |       .use(
 81 |         "/204",
 82 |         eventHandler(() => null) // eslint-disable-line unicorn/no-null
 83 |       )
 84 |       .use(
 85 |         "/timeout",
 86 |         eventHandler(async () => {
 87 |           await new Promise((resolve) => {
 88 |             setTimeout(() => {
 89 |               resolve(createError({ status: 408 }));
 90 |             }, 1000 * 5);
 91 |           });
 92 |         })
 93 |       );
 94 | 
 95 |     listener = await listen(toNodeListener(app));
 96 |   });
 97 | 
 98 |   afterAll(() => {
 99 |     listener.close().catch(console.error);
100 |   });
101 | 
102 |   beforeEach(() => {
103 |     fetch.mockClear();
104 |   });
105 | 
106 |   it("ok", async () => {
107 |     expect(await $fetch(getURL("ok"))).to.equal("ok");
108 |   });
109 | 
110 |   it("custom parseResponse", async () => {
111 |     let called = 0;
112 |     const parser = (r) => {
113 |       called++;
114 |       return "C" + r;
115 |     };
116 |     expect(await $fetch(getURL("ok"), { parseResponse: parser })).to.equal(
117 |       "Cok"
118 |     );
119 |     expect(called).to.equal(1);
120 |   });
121 | 
122 |   it("allows specifying FetchResponse method", async () => {
123 |     expect(
124 |       await $fetch(getURL("params?test=true"), { responseType: "json" })
125 |     ).to.deep.equal({ test: "true" });
126 |     expect(
127 |       await $fetch(getURL("params?test=true"), { responseType: "blob" })
128 |     ).to.be.instanceOf(Blob);
129 |     expect(
130 |       await $fetch(getURL("params?test=true"), { responseType: "text" })
131 |     ).to.equal('{"test":"true"}');
132 |     expect(
133 |       await $fetch(getURL("params?test=true"), { responseType: "arrayBuffer" })
134 |     ).to.be.instanceOf(ArrayBuffer);
135 |   });
136 | 
137 |   it("returns a blob for binary content-type", async () => {
138 |     expect(await $fetch(getURL("binary"))).to.be.instanceOf(Blob);
139 |   });
140 | 
141 |   it("baseURL", async () => {
142 |     expect(await $fetch("/x?foo=123", { baseURL: getURL("url") })).to.equal(
143 |       "/x?foo=123"
144 |     );
145 |   });
146 | 
147 |   it("stringifies posts body automatically", async () => {
148 |     const { body } = await $fetch(getURL("post"), {
149 |       method: "POST",
150 |       body: { num: 42 },
151 |     });
152 |     expect(body).to.deep.eq({ num: 42 });
153 | 
154 |     const body2 = (
155 |       await $fetch(getURL("post"), {
156 |         method: "POST",
157 |         body: [{ num: 42 }, { num: 43 }],
158 |       })
159 |     ).body;
160 |     expect(body2).to.deep.eq([{ num: 42 }, { num: 43 }]);
161 | 
162 |     const headerFetches = [
163 |       [["X-header", "1"]],
164 |       { "x-header": "1" },
165 |       new Headers({ "x-header": "1" }),
166 |     ];
167 | 
168 |     for (const sentHeaders of headerFetches) {
169 |       const { headers } = await $fetch(getURL("post"), {
170 |         method: "POST",
171 |         body: { num: 42 },
172 |         headers: sentHeaders as HeadersInit,
173 |       });
174 |       expect(headers).to.include({ "x-header": "1" });
175 |       expect(headers).to.include({ accept: "application/json" });
176 |     }
177 |   });
178 | 
179 |   it("does not stringify body when content type != application/json", async () => {
180 |     const message = '"Hallo von Pascal"';
181 |     const { body } = await $fetch(getURL("echo"), {
182 |       method: "POST",
183 |       body: message,
184 |       headers: { "Content-Type": "text/plain" },
185 |     });
186 |     expect(body).to.deep.eq(message);
187 |   });
188 | 
189 |   it("Handle Buffer body", async () => {
190 |     const message = "Hallo von Pascal";
191 |     const { body } = await $fetch(getURL("echo"), {
192 |       method: "POST",
193 |       body: Buffer.from("Hallo von Pascal"),
194 |       headers: { "Content-Type": "text/plain" },
195 |     });
196 |     expect(body).to.deep.eq(message);
197 |   });
198 | 
199 |   it.skipIf(Number(nodeMajorVersion) < 18)(
200 |     "Handle ReadableStream body",
201 |     async () => {
202 |       const message = "Hallo von Pascal";
203 |       const { body } = await $fetch(getURL("echo"), {
204 |         method: "POST",
205 |         headers: {
206 |           "content-length": "16",
207 |         },
208 |         body: new ReadableStream({
209 |           start(controller) {
210 |             controller.enqueue(new TextEncoder().encode(message));
211 |             controller.close();
212 |           },
213 |         }),
214 |       });
215 |       expect(body).to.deep.eq(message);
216 |     }
217 |   );
218 | 
219 |   it.skipIf(Number(nodeMajorVersion) < 18)("Handle Readable body", async () => {
220 |     const message = "Hallo von Pascal";
221 |     const { body } = await $fetch(getURL("echo"), {
222 |       method: "POST",
223 |       headers: {
224 |         "content-length": "16",
225 |       },
226 |       body: new Readable({
227 |         read() {
228 |           this.push(message);
229 |           this.push(null); // eslint-disable-line unicorn/no-null
230 |         },
231 |       }),
232 |     });
233 |     expect(body).to.deep.eq(message);
234 |   });
235 | 
236 |   it("Bypass FormData body", async () => {
237 |     const data = new FormData();
238 |     data.append("foo", "bar");
239 |     const { body } = await $fetch(getURL("post"), {
240 |       method: "POST",
241 |       body: data,
242 |     });
243 |     expect(body).to.include('form-data; name="foo"');
244 |   });
245 | 
246 |   it("Bypass URLSearchParams body", async () => {
247 |     const data = new URLSearchParams({ foo: "bar" });
248 |     const { body } = await $fetch(getURL("post"), {
249 |       method: "POST",
250 |       body: data,
251 |     });
252 |     expect(body).toMatchObject({ foo: "bar" });
253 |   });
254 | 
255 |   it("404", async () => {
256 |     const error = await $fetch(getURL("404")).catch((error_) => error_);
257 |     expect(error.toString()).to.contain("Cannot find any path matching /404.");
258 |     expect(error.data).to.deep.eq({
259 |       stack: [],
260 |       statusCode: 404,
261 |       statusMessage: "Cannot find any path matching /404.",
262 |     });
263 |     expect(error.response?._data).to.deep.eq(error.data);
264 |     expect(error.request).to.equal(getURL("404"));
265 |   });
266 | 
267 |   it("403 with ignoreResponseError", async () => {
268 |     const res = await $fetch(getURL("403"), { ignoreResponseError: true });
269 |     expect(res?.statusCode).to.eq(403);
270 |     expect(res?.statusMessage).to.eq("Forbidden");
271 |   });
272 | 
273 |   it("204 no content", async () => {
274 |     const res = await $fetch(getURL("204"));
275 |     expect(res).toBeUndefined();
276 |   });
277 | 
278 |   it("HEAD no content", async () => {
279 |     const res = await $fetch(getURL("/ok"), { method: "HEAD" });
280 |     expect(res).toBeUndefined();
281 |   });
282 | 
283 |   it("baseURL with retry", async () => {
284 |     const error = await $fetch("", { baseURL: getURL("404"), retry: 3 }).catch(
285 |       (error_) => error_
286 |     );
287 |     expect(error.request).to.equal(getURL("404"));
288 |   });
289 | 
290 |   it("retry with number delay", async () => {
291 |     const slow = $fetch<string>(getURL("408"), {
292 |       retry: 2,
293 |       retryDelay: 100,
294 |     }).catch(() => "slow");
295 |     const fast = $fetch<string>(getURL("408"), {
296 |       retry: 2,
297 |       retryDelay: 1,
298 |     }).catch(() => "fast");
299 | 
300 |     const race = await Promise.race([slow, fast]);
301 |     expect(race).to.equal("fast");
302 |   });
303 | 
304 |   it("retry with callback delay", async () => {
305 |     const slow = $fetch<string>(getURL("408"), {
306 |       retry: 2,
307 |       retryDelay: () => 100,
308 |     }).catch(() => "slow");
309 |     const fast = $fetch<string>(getURL("408"), {
310 |       retry: 2,
311 |       retryDelay: () => 1,
312 |     }).catch(() => "fast");
313 | 
314 |     const race = await Promise.race([slow, fast]);
315 |     expect(race).to.equal("fast");
316 |   });
317 | 
318 |   it("abort with retry", () => {
319 |     const controller = new AbortController();
320 |     async function abortHandle() {
321 |       controller.abort();
322 |       const response = await $fetch("", {
323 |         baseURL: getURL("ok"),
324 |         retry: 3,
325 |         signal: controller.signal,
326 |       });
327 |       console.log("response", response);
328 |     }
329 |     expect(abortHandle()).rejects.toThrow(/aborted/);
330 |   });
331 | 
332 |   it("passing request obj should return request obj in error", async () => {
333 |     const error = await $fetch(getURL("/403"), { method: "post" }).catch(
334 |       (error) => error
335 |     );
336 |     expect(error.toString()).toBe(
337 |       'FetchError: [POST] "http://localhost:3000/403": 403 Forbidden'
338 |     );
339 |     expect(error.request).to.equal(getURL("403"));
340 |     expect(error.options.method).to.equal("POST");
341 |     expect(error.response?._data).to.deep.eq(error.data);
342 |   });
343 | 
344 |   it("aborting on timeout", async () => {
345 |     const noTimeout = $fetch(getURL("timeout")).catch(() => "no timeout");
346 |     const timeout = $fetch(getURL("timeout"), {
347 |       timeout: 100,
348 |       retry: 0,
349 |     }).catch(() => "timeout");
350 |     const race = await Promise.race([noTimeout, timeout]);
351 |     expect(race).to.equal("timeout");
352 |   });
353 | 
354 |   it("aborting on timeout reason", async () => {
355 |     await $fetch(getURL("timeout"), {
356 |       timeout: 100,
357 |       retry: 0,
358 |     }).catch((error) => {
359 |       expect(error.cause.message).to.include(
360 |         "The operation was aborted due to timeout"
361 |       );
362 |       expect(error.cause.name).to.equal("TimeoutError");
363 |       expect(error.cause.code).to.equal(DOMException.TIMEOUT_ERR);
364 |     });
365 |   });
366 | 
367 |   it("deep merges defaultOptions", async () => {
368 |     const _customFetch = $fetch.create({
369 |       query: {
370 |         a: 0,
371 |       },
372 |       params: {
373 |         b: 2,
374 |       },
375 |       headers: {
376 |         "x-header-a": "0",
377 |         "x-header-b": "2",
378 |       },
379 |     });
380 |     const { headers, path } = await _customFetch(getURL("echo"), {
381 |       query: {
382 |         a: 1,
383 |       },
384 |       params: {
385 |         c: 3,
386 |       },
387 |       headers: {
388 |         "Content-Type": "text/plain",
389 |         "x-header-a": "1",
390 |         "x-header-c": "3",
391 |       },
392 |     });
393 | 
394 |     expect(headers).to.include({
395 |       "x-header-a": "1",
396 |       "x-header-b": "2",
397 |       "x-header-c": "3",
398 |     });
399 | 
400 |     const parseParams = (str: string) =>
401 |       Object.fromEntries(new URLSearchParams(str).entries());
402 |     expect(parseParams(path)).toMatchObject(parseParams("?b=2&c=3&a=1"));
403 |   });
404 | 
405 |   it("uses request headers", async () => {
406 |     expect(
407 |       await $fetch(
408 |         new Request(getURL("echo"), { headers: { foo: "1" } }),
409 |         {}
410 |       ).then((r) => r.headers)
411 |     ).toMatchObject({ foo: "1" });
412 | 
413 |     expect(
414 |       await $fetch(new Request(getURL("echo"), { headers: { foo: "1" } }), {
415 |         headers: { foo: "2", bar: "3" },
416 |       }).then((r) => r.headers)
417 |     ).toMatchObject({ foo: "2", bar: "3" });
418 |   });
419 | 
420 |   it("hook errors", async () => {
421 |     // onRequest
422 |     await expect(
423 |       $fetch(getURL("/ok"), {
424 |         onRequest: () => {
425 |           throw new Error("error in onRequest");
426 |         },
427 |       })
428 |     ).rejects.toThrow("error in onRequest");
429 | 
430 |     // onRequestError
431 |     await expect(
432 |       $fetch("/" /* non absolute is not acceptable */, {
433 |         onRequestError: () => {
434 |           throw new Error("error in onRequestError");
435 |         },
436 |       })
437 |     ).rejects.toThrow("error in onRequestError");
438 | 
439 |     // onResponse
440 |     await expect(
441 |       $fetch(getURL("/ok"), {
442 |         onResponse: () => {
443 |           throw new Error("error in onResponse");
444 |         },
445 |       })
446 |     ).rejects.toThrow("error in onResponse");
447 | 
448 |     // onResponseError
449 |     await expect(
450 |       $fetch(getURL("/403"), {
451 |         onResponseError: () => {
452 |           throw new Error("error in onResponseError");
453 |         },
454 |       })
455 |     ).rejects.toThrow("error in onResponseError");
456 |   });
457 | 
458 |   it("calls hooks", async () => {
459 |     const onRequest = vi.fn();
460 |     const onRequestError = vi.fn();
461 |     const onResponse = vi.fn();
462 |     const onResponseError = vi.fn();
463 | 
464 |     await $fetch(getURL("/ok"), {
465 |       onRequest,
466 |       onRequestError,
467 |       onResponse,
468 |       onResponseError,
469 |     });
470 | 
471 |     expect(onRequest).toHaveBeenCalledOnce();
472 |     expect(onRequestError).not.toHaveBeenCalled();
473 |     expect(onResponse).toHaveBeenCalledOnce();
474 |     expect(onResponseError).not.toHaveBeenCalled();
475 | 
476 |     onRequest.mockReset();
477 |     onRequestError.mockReset();
478 |     onResponse.mockReset();
479 |     onResponseError.mockReset();
480 | 
481 |     await $fetch(getURL("/403"), {
482 |       onRequest,
483 |       onRequestError,
484 |       onResponse,
485 |       onResponseError,
486 |     }).catch((error) => error);
487 | 
488 |     expect(onRequest).toHaveBeenCalledOnce();
489 |     expect(onRequestError).not.toHaveBeenCalled();
490 |     expect(onResponse).toHaveBeenCalledOnce();
491 |     expect(onResponseError).toHaveBeenCalledOnce();
492 | 
493 |     onRequest.mockReset();
494 |     onRequestError.mockReset();
495 |     onResponse.mockReset();
496 |     onResponseError.mockReset();
497 | 
498 |     await $fetch(getURL("/ok"), {
499 |       onRequest: [onRequest, onRequest],
500 |       onRequestError: [onRequestError, onRequestError],
501 |       onResponse: [onResponse, onResponse],
502 |       onResponseError: [onResponseError, onResponseError],
503 |     });
504 | 
505 |     expect(onRequest).toHaveBeenCalledTimes(2);
506 |     expect(onRequestError).not.toHaveBeenCalled();
507 |     expect(onResponse).toHaveBeenCalledTimes(2);
508 |     expect(onResponseError).not.toHaveBeenCalled();
509 | 
510 |     onRequest.mockReset();
511 |     onRequestError.mockReset();
512 |     onResponse.mockReset();
513 |     onResponseError.mockReset();
514 | 
515 |     await $fetch(getURL("/403"), {
516 |       onRequest: [onRequest, onRequest],
517 |       onRequestError: [onRequestError, onRequestError],
518 |       onResponse: [onResponse, onResponse],
519 |       onResponseError: [onResponseError, onResponseError],
520 |     }).catch((error) => error);
521 | 
522 |     expect(onRequest).toHaveBeenCalledTimes(2);
523 |     expect(onRequestError).not.toHaveBeenCalled();
524 |     expect(onResponse).toHaveBeenCalledTimes(2);
525 |     expect(onResponseError).toHaveBeenCalledTimes(2);
526 |   });
527 | 
528 |   it("default fetch options", async () => {
529 |     await $fetch("https://jsonplaceholder.typicode.com/todos/1", {});
530 |     expect(fetch).toHaveBeenCalledOnce();
531 |     const options = fetch.mock.calls[0][1];
532 |     expect(options).toStrictEqual({
533 |       headers: expect.any(Headers),
534 |     });
535 |   });
536 | });
537 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ESNext",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "Node",
 6 |     "esModuleInterop": true,
 7 |     "outDir": "dist",
 8 |     "strict": true,
 9 |     "declaration": true,
10 |     "types": ["node"]
11 |   },
12 |   "include": ["src"]
13 | }
14 | 


--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from "vitest/config";
 2 | 
 3 | export default defineConfig({
 4 |   test: {
 5 |     coverage: {
 6 |       reporter: ["text", "clover", "json"],
 7 |     },
 8 |   },
 9 | });
10 | 


--------------------------------------------------------------------------------