├── .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 | --------------------------------------------------------------------------------