├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── lock.yml │ ├── node.js.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── bower.json ├── fetch.js ├── fetch.js.flow ├── package.json ├── prettier.config.js ├── rollup.config.js └── test ├── karma-worker.config.js ├── karma.config.js ├── server.js ├── test.js ├── worker-adapter.js └── worker.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | open-pull-requests-limit: 1 12 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | lock: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 17 | with: 18 | github-token: ${{ github.token }} 19 | issue-lock-inactive-days: '180' 20 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [18.x, 20.x] 23 | 24 | steps: 25 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | - run: npm i 31 | - run: npm run build --if-present 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | name: release-please 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: google-github-actions/release-please-action@a37ac6e4f6449ce8b3f7607e4d97d0146028dc0b # v4.1.0 17 | with: 18 | release-type: node 19 | package-name: release-please-action 20 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '17 14 * * 1' 14 | push: 15 | branches: [ main ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | security-events: write # to upload the results to code-scanning dashboard 26 | id-token: write # to publish results and get a badge (see publish_results below) 27 | 28 | steps: 29 | - name: "Checkout code" 30 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v3.1.0 31 | with: 32 | persist-credentials: false 33 | 34 | - name: "Run analysis" 35 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 36 | with: 37 | results_file: results.sarif 38 | results_format: sarif 39 | # Publish results to OpenSSF REST API for easy access by consumers and 40 | # allows the repository to include the Scorecard badge. 41 | # See https://github.com/ossf/scorecard-action#publishing-results. 42 | publish_results: true 43 | 44 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 45 | # format to the repository Actions tab. 46 | - name: "Upload artifact" 47 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 48 | with: 49 | name: SARIF file 50 | path: results.sarif 51 | retention-days: 5 52 | 53 | # Upload the results to GitHub's code scanning dashboard. 54 | - name: "Upload to code-scanning" 55 | uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 56 | with: 57 | sarif_file: results.sarif 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | package-lock.json 3 | dist/ 4 | bower_components/ 5 | node_modules/ 6 | sauce_connect/ 7 | sauce_connect.log 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | package-lock.json 3 | bower_components/ 4 | node_modules/ 5 | sauce_connect/ 6 | sauce_connect.log 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | ## [3.6.20](https://github.com/JakeChampion/fetch/compare/v3.6.19...v3.6.20) (2023-12-13) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Response.error().ok === false ([#1412](https://github.com/JakeChampion/fetch/issues/1412)) ([27e1c75](https://github.com/JakeChampion/fetch/commit/27e1c75f830f0b70a40b511e03652776951aca75)) 11 | 12 | ## [3.6.19](https://github.com/JakeChampion/fetch/compare/v3.6.18...v3.6.19) (2023-09-11) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * Have unique error messages for xhr timeouts and errors ([#1380](https://github.com/JakeChampion/fetch/issues/1380)) ([7170f0b](https://github.com/JakeChampion/fetch/commit/7170f0b127d16c5895aba61c9168482834809046)) 18 | 19 | 20 | ## [v3.6.18](https://github.com/JakeChampion/fetch/compare/v3.6.17...v3.6.18) 21 | 22 | - Fix - File fetching broken since commit 0c1d2b9 [`#1375`](https://github.com/JakeChampion/fetch/pull/1375) 23 | - Remove broken links [`1dc07c6`](https://github.com/JakeChampion/fetch/commit/1dc07c6064a32e989306fb2324204c56c93140fe) 24 | - automatically generate a changelog [`0e7d1dd`](https://github.com/JakeChampion/fetch/commit/0e7d1dd95826b3b76510f0832784207f2609145e) 25 | 26 | ## [v3.6.17](https://github.com/JakeChampion/fetch/compare/v3.6.16...v3.6.17) 27 | 28 | > 20 July 2023 29 | 30 | - Revert "Resolves https://github.com/JakeChampion/fetch/issues/928" [`#928`](https://github.com/JakeChampion/fetch/issues/928) 31 | 32 | ## [v3.6.16](https://github.com/JakeChampion/fetch/compare/v3.6.15...v3.6.16) 33 | 34 | > 18 July 2023 35 | 36 | - Resolves https://github.com/JakeChampion/fetch/issues/928 [`#928`](https://github.com/JakeChampion/fetch/issues/928) 37 | 38 | ## [v3.6.15](https://github.com/JakeChampion/fetch/compare/v3.6.14...v3.6.15) 39 | 40 | > 18 July 2023 41 | 42 | - fix https://github.com/JakeChampion/fetch/issues/997 [`#997`](https://github.com/JakeChampion/fetch/issues/997) 43 | 44 | ## [v3.6.14](https://github.com/JakeChampion/fetch/compare/v3.6.13...v3.6.14) 45 | 46 | > 18 July 2023 47 | 48 | - Fix https://github.com/JakeChampion/fetch/issues/1076 [`#1076`](https://github.com/JakeChampion/fetch/issues/1076) 49 | 50 | ## [v3.6.13](https://github.com/JakeChampion/fetch/compare/v3.6.12...v3.6.13) 51 | 52 | > 18 July 2023 53 | 54 | - respect charset within readBlobAsText [`#1059`](https://github.com/JakeChampion/fetch/issues/1059) 55 | 56 | ## [v3.6.12](https://github.com/JakeChampion/fetch/compare/v3.6.11...v3.6.12) 57 | 58 | > 18 July 2023 59 | 60 | - fix: Headers only accepts array which have nested array of length 2 [`#1235`](https://github.com/JakeChampion/fetch/issues/1235) 61 | 62 | ## [v3.6.11](https://github.com/JakeChampion/fetch/compare/v3.6.10...v3.6.11) 63 | 64 | > 18 July 2023 65 | 66 | - Define Body.arrayBuffer even if support.blob is false [`#992`](https://github.com/JakeChampion/fetch/issues/992) 67 | 68 | ## [v3.6.10](https://github.com/JakeChampion/fetch/compare/v3.6.9...v3.6.10) 69 | 70 | > 18 July 2023 71 | 72 | - use globals if they exist [`dffc542`](https://github.com/JakeChampion/fetch/commit/dffc542fe7140f35ee7fec29e3da67f3bf080910) 73 | 74 | ## [v3.6.9](https://github.com/JakeChampion/fetch/compare/v3.6.8...v3.6.9) 75 | 76 | > 18 July 2023 77 | 78 | - fix: when no body supplied, do not set bodyUsed to true [`7d92dff`](https://github.com/JakeChampion/fetch/commit/7d92dff12d7c4058b57c7e77adeb0a76ffab639f) 79 | 80 | ## [v3.6.8](https://github.com/JakeChampion/fetch/compare/v3.6.7...v3.6.8) 81 | 82 | > 18 July 2023 83 | 84 | - validate status is in range [`#1213`](https://github.com/JakeChampion/fetch/issues/1213) 85 | 86 | ## [v3.6.7](https://github.com/JakeChampion/fetch/compare/v3.6.6...v3.6.7) 87 | 88 | > 18 July 2023 89 | 90 | - dont shadow `global` [`#1026`](https://github.com/JakeChampion/fetch/issues/1026) 91 | - dont use github eslint [`408d3b6`](https://github.com/JakeChampion/fetch/commit/408d3b60e27abef325dd898d899430c46a0012b2) 92 | - remove invalid-headers test [`e3f6590`](https://github.com/JakeChampion/fetch/commit/e3f65907924b7692af7c08cd92044456bc92ad8b) 93 | - Update lock.yml permissions [`e97321b`](https://github.com/JakeChampion/fetch/commit/e97321bc081e80275397fc4c7a990791aa8b3524) 94 | 95 | ## [v3.6.6](https://github.com/JakeChampion/fetch/compare/v3.6.5...v3.6.6) 96 | 97 | > 18 July 2023 98 | 99 | - fix: ignore not throw on invalid response headers [`#930`](https://github.com/JakeChampion/fetch/issues/930) 100 | 101 | ## [v3.6.5](https://github.com/JakeChampion/fetch/compare/v3.6.4...v3.6.5) 102 | 103 | > 18 July 2023 104 | 105 | - Add some missed methods which should be normalized as uppercase [`a43b628`](https://github.com/JakeChampion/fetch/commit/a43b6283833c403230bb1a5238e2d7ac435c52da) 106 | - Update caniuse link to use HTTPS and new pattern [`fb5b0cf`](https://github.com/JakeChampion/fetch/commit/fb5b0cf42b470faf8c5448ab461d561f34380a30) 107 | 108 | ## [v3.6.4](https://github.com/JakeChampion/fetch/compare/v3.6.3...v3.6.4) 109 | 110 | > 18 July 2023 111 | 112 | - always set a signal on Request [`d1d09fb`](https://github.com/JakeChampion/fetch/commit/d1d09fb8039b4b8c7f2f5d6c844ea72d8a3cefe6) 113 | 114 | ## [v3.6.3](https://github.com/JakeChampion/fetch/compare/v3.6.2...v3.6.3) 115 | 116 | > 18 July 2023 117 | 118 | - Compatible global equals to the false [`7727e50`](https://github.com/JakeChampion/fetch/commit/7727e50493eafae9a7005f10f18f81e5bbcbfdd3) 119 | 120 | ## [v3.6.2](https://github.com/JakeChampion/fetch/compare/v3.6.1...v3.6.2) 121 | 122 | > 27 February 2021 123 | 124 | - Revert "Represent non-stringified JSON request body as an [object Object] string" [`e42f201`](https://github.com/JakeChampion/fetch/commit/e42f201b8b0af8b3f2615abe8161c8087f52f1b2) 125 | 126 | ## [v3.6.1](https://github.com/JakeChampion/fetch/compare/v3.6.0...v3.6.1) 127 | 128 | > 18 February 2021 129 | 130 | - Fix MSIE compatibility [`da97bdb`](https://github.com/JakeChampion/fetch/commit/da97bdb462632288b21eeca67fc6b93c7077ebae) 131 | - use var instead of const [`5d3952d`](https://github.com/JakeChampion/fetch/commit/5d3952d10736a98a550043b933c50800643e2756) 132 | - Restore package.json [`6b4bd97`](https://github.com/JakeChampion/fetch/commit/6b4bd971b1e415a347cf20db4b925d1b845669a9) 133 | 134 | ## [v3.6.0](https://github.com/JakeChampion/fetch/compare/v3.5.0...v3.6.0) 135 | 136 | > 18 February 2021 137 | 138 | - Fix statusText: undefined should give '' and null should give 'null' [`b5c8bd0`](https://github.com/JakeChampion/fetch/commit/b5c8bd0fee1530f1c204cc5c68b427a3498dbdad) 139 | - Represent non-stringified JSON request body as an [object Object] string [`5c6b055`](https://github.com/JakeChampion/fetch/commit/5c6b055e6ae6f718f416c94bfcdc89693d0abdcb) 140 | - Fix eslint and eslint-plugin-github dependency conflicts [`190e698`](https://github.com/JakeChampion/fetch/commit/190e698f8e737ad751a11de60f6b8b3301fa557b) 141 | 142 | ## [v3.5.0](https://github.com/JakeChampion/fetch/compare/v3.4.1...v3.5.0) 143 | 144 | > 6 November 2020 145 | 146 | - Fixes #748 [`#748`](https://github.com/JakeChampion/fetch/issues/748) 147 | - Create lock.yml [`8767781`](https://github.com/JakeChampion/fetch/commit/87677811d543cfb44b124e026b50f710e95017ec) 148 | 149 | ## [v3.4.1](https://github.com/JakeChampion/fetch/compare/v3.4.0...v3.4.1) 150 | 151 | > 7 September 2020 152 | 153 | - Add npmignore file to ensure we always publish the dist directory [`7ca02eb`](https://github.com/JakeChampion/fetch/commit/7ca02eb0234b0a61fd711d922b2e69d3c5390516) 154 | - Make the clean task remove the dist directory and the default task create it [`fd23745`](https://github.com/JakeChampion/fetch/commit/fd23745f3474cd23d88e5128d8bc74813be1aff0) 155 | 156 | ## [v3.4.0](https://github.com/JakeChampion/fetch/compare/v3.3.1...v3.4.0) 157 | 158 | > 7 August 2020 159 | 160 | - Use globalThis as the global object if it exists [`96c2651`](https://github.com/JakeChampion/fetch/commit/96c26512608a0081d493df4fc17da4394bd1b410) 161 | 162 | ## [v3.3.1](https://github.com/JakeChampion/fetch/compare/v3.3.0...v3.3.1) 163 | 164 | > 4 August 2020 165 | 166 | - rename variable to no longer shadow over function of same name [`c5db762`](https://github.com/JakeChampion/fetch/commit/c5db7621c3b1530683b8f706388d4ac210a2db02) 167 | - remove semicolon to pass linting [`f264aa5`](https://github.com/JakeChampion/fetch/commit/f264aa5704f7431c429ec16e6fdd3c7034c7f2d9) 168 | 169 | ## [v3.3.0](https://github.com/JakeChampion/fetch/compare/v3.2.0...v3.3.0) 170 | 171 | > 4 August 2020 172 | 173 | - Make Response.arrayBuffer() always resolve with a `ArrayBuffer` [`#801`](https://github.com/github/fetch/issues/801) 174 | - Stop using top-level `this` to stop rollup warning [`#802`](https://github.com/github/fetch/issues/802) 175 | - Recommend an AbortController polyfill which is fully synchronous [`#800`](https://github.com/github/fetch/issues/800) 176 | - Add keepalive caveat [`#780`](https://github.com/github/fetch/issues/780) 177 | - Throw a TypeError if Request or Response functions are called without `new` [`5ef028d`](https://github.com/JakeChampion/fetch/commit/5ef028d61f6c1543603cdacbe0f8a0f00d5957c0) 178 | - If headers are passed in via a Record then do not normalise the header names as part of the request [`b65ed60`](https://github.com/JakeChampion/fetch/commit/b65ed608604492d605df2d62cd4c5050e2a8d508) 179 | - Update fetch.js [`37b55c2`](https://github.com/JakeChampion/fetch/commit/37b55c27413b902cef4e629892424ae469fb1ea2) 180 | 181 | ## [v3.2.0](https://github.com/JakeChampion/fetch/compare/v3.1.1...v3.2.0) 182 | 183 | > 9 July 2020 184 | 185 | - Detect if DOMException exists via typeof instead of trying to call it and catching the exception which may get thrown [`#724`](https://github.com/github/fetch/issues/724) 186 | - use `this` if `self` is not defined [`#657`](https://github.com/github/fetch/issues/657) 187 | - create variable called `global` which is either `self` or `this` [`a0783a5`](https://github.com/JakeChampion/fetch/commit/a0783a5571018191578cc08d5b3bac61a0b64562) 188 | - Add support for no-cache and no-store via a cache-busting querystring parameter [`a0dcd85`](https://github.com/JakeChampion/fetch/commit/a0dcd853f8ed29d06a022f92c87c303bd0e1f1bf) 189 | - make global `this` correct when using rollup [`6e9fc0e`](https://github.com/JakeChampion/fetch/commit/6e9fc0ee026dd89d864c3d176c57789ee5615114) 190 | 191 | ## [v3.1.1](https://github.com/JakeChampion/fetch/compare/v3.1.0...v3.1.1) 192 | 193 | > 8 July 2020 194 | 195 | - check if Content-Type header exists prior to examining the value [`#792`](https://github.com/JakeChampion/fetch/pull/792) 196 | - Move from Travis to GitHub Actions [`#793`](https://github.com/JakeChampion/fetch/pull/793) 197 | 198 | ## [v3.1.0](https://github.com/JakeChampion/fetch/compare/v3.0.1...v3.1.0) 199 | 200 | > 29 June 2020 201 | 202 | ## [v3.0.1](https://github.com/JakeChampion/fetch/compare/v3.0.0...v3.0.1) 203 | 204 | > 8 July 2020 205 | 206 | - check if Content-Type header exists prior to examining the value [`#792`](https://github.com/JakeChampion/fetch/pull/792) 207 | - Move from Travis to GitHub Actions [`#793`](https://github.com/JakeChampion/fetch/pull/793) 208 | - Co-authored-by: Jake Champion <me@jakechampion.name> [`#575`](https://github.com/JakeChampion/fetch/pull/575) 209 | - work around IE XHR bug with '' URL Fixes #618 [`#619`](https://github.com/JakeChampion/fetch/pull/619) 210 | - Allow exclamation mark as valid header character [`#745`](https://github.com/JakeChampion/fetch/pull/745) 211 | - Avoid blob conversion for specific requests [`#752`](https://github.com/JakeChampion/fetch/pull/752) 212 | - Compatibility for fetch-mock using proxy-pollyfill [`#736`](https://github.com/JakeChampion/fetch/pull/736) 213 | - Change default statusText for Response [`#698`](https://github.com/JakeChampion/fetch/pull/698) 214 | - Document more common pitfalls in the README [`#734`](https://github.com/JakeChampion/fetch/pull/734) 215 | - field name can not by empty [`#684`](https://github.com/JakeChampion/fetch/pull/684) 216 | - work around IE XHR bug with '' URL Fixes #618 (#619) [`#618`](https://github.com/JakeChampion/fetch/issues/618) 217 | - Clarify what parts of the standard we don't want to implement [`#661`](https://github.com/JakeChampion/fetch/issues/661) 218 | - Document more caveats [`9a0bce2`](https://github.com/JakeChampion/fetch/commit/9a0bce23454cdd5beefd9d4c599664003573e581) 219 | - Fix issue #533 [`7f030fa`](https://github.com/JakeChampion/fetch/commit/7f030fab4d79433204331cefe365f5fbbab9e992) 220 | - Compatibility with newer eslint-plugin-github [`1821b74`](https://github.com/JakeChampion/fetch/commit/1821b74b808152d4d6e787c21165f2d569c2a7c4) 221 | 222 | 223 | ### [v3.0.0](https://github.com/JakeChampion/fetch/compare/v2.0.4...v3.0.0) 224 | 225 | > 7 September 2018 226 | 227 | - Add flow definitions [`#654`](https://github.com/JakeChampion/fetch/pull/654) 228 | - Match spec behavior re: unsupported body type [`#651`](https://github.com/JakeChampion/fetch/pull/651) 229 | - Update Karma and detect available browsers when testing [`#652`](https://github.com/JakeChampion/fetch/pull/652) 230 | - Adopt Contributor Covenant Code of Conduct [`#649`](https://github.com/JakeChampion/fetch/pull/649) 231 | - Change `credentials` default value to `same-origin` [`#640`](https://github.com/JakeChampion/fetch/pull/640) 232 | - Switch test suite from PhantomJS to Karma [`#626`](https://github.com/JakeChampion/fetch/pull/626) 233 | - Support abort API [`#592`](https://github.com/JakeChampion/fetch/pull/592) 234 | - build/distribute as UMD [`#616`](https://github.com/JakeChampion/fetch/pull/616) 235 | - Test signal reuse. Add AbortSignal polyfill. [`#2`](https://github.com/JakeChampion/fetch/pull/2) 236 | - Clear abort event listener for all xhr completion states. [`#1`](https://github.com/JakeChampion/fetch/pull/1) 237 | - Expand install & importing documentation [`#569`](https://github.com/JakeChampion/fetch/issues/569) 238 | - Match spec behavior re: unsupported body type [`#576`](https://github.com/JakeChampion/fetch/issues/576) 239 | - Run test files through prettier [`0a57487`](https://github.com/JakeChampion/fetch/commit/0a5748775d99f882172375693f56761383f8faf3) 240 | - Unwrap `fetch.js` to be a clean module file [`8aec47c`](https://github.com/JakeChampion/fetch/commit/8aec47cb6c67a9a321f1eb07457f70fc46235610) 241 | - Switch from PhantomJS to Karma + Chrome/Firefox for testing [`b539589`](https://github.com/JakeChampion/fetch/commit/b53958904649bfeb784083b9b7e0b89902c7d30e) 242 | 243 | ## [v2.0.4](https://github.com/JakeChampion/fetch/compare/v2.0.3...v2.0.4) 244 | 245 | > 29 March 2018 246 | 247 | - Create CONTRIBUTING.md [`#604`](https://github.com/JakeChampion/fetch/pull/604) 248 | - Tweak the wording of the “Read this first” section [`#553`](https://github.com/JakeChampion/fetch/pull/553) 249 | - Allow undefined Response status [`#534`](https://github.com/JakeChampion/fetch/pull/534) 250 | - Ensure cookies aren't sent if `credentials: omit` [`#526`](https://github.com/JakeChampion/fetch/pull/526) 251 | - Added yarn command as option to installation [`#492`](https://github.com/JakeChampion/fetch/pull/492) 252 | - Add global replace for processing raw headers [`#496`](https://github.com/JakeChampion/fetch/pull/496) 253 | - Added safari to native fetch browser support. [`#469`](https://github.com/JakeChampion/fetch/pull/469) 254 | - Support obs-fold as header delimiter [`#491`](https://github.com/JakeChampion/fetch/pull/491) 255 | - Tweak the wording of "Read this first" [`54dc3f8`](https://github.com/JakeChampion/fetch/commit/54dc3f823fe3e6452da8d19bf7aad7eda4cd1dd8) 256 | - Add test for undefined Response status [`0ecdd40`](https://github.com/JakeChampion/fetch/commit/0ecdd40c50d4bcdfd4d2a09448a6d01089dc182a) 257 | - Fix cookie test with newer versions of Node [`7831671`](https://github.com/JakeChampion/fetch/commit/7831671b172435c52064f588cf7145236fecf5f2) 258 | 259 | ## [v2.0.3](https://github.com/JakeChampion/fetch/compare/v2.0.2...v2.0.3) 260 | 261 | > 2 March 2017 262 | 263 | - Accept array in Headers constructor [`#485`](https://github.com/JakeChampion/fetch/pull/485) 264 | - Improve README language [`#483`](https://github.com/JakeChampion/fetch/pull/483) 265 | - Fix grammar mistake in README [`#468`](https://github.com/JakeChampion/fetch/pull/468) 266 | - Remove bower version from release instructions [`5cc72dd`](https://github.com/JakeChampion/fetch/commit/5cc72dd734bfd459a61a61e472c90654d71afc91) 267 | - Remove extra punctuation [`eebaa2a`](https://github.com/JakeChampion/fetch/commit/eebaa2a1bc21eeba98ee00c9f94a0a4c2007cff1) 268 | - Fetch 2.0.3 [`d4ed806`](https://github.com/JakeChampion/fetch/commit/d4ed806fdcbdeaef707d27f6c88943f0336a647d) 269 | 270 | ## [v2.0.2](https://github.com/JakeChampion/fetch/compare/v2.0.1...v2.0.2) 271 | 272 | > 19 January 2017 273 | 274 | - Treat any non-Request arg to `new Request()` as string url [`#465`](https://github.com/JakeChampion/fetch/pull/465) 275 | - Support Tolerance Provision when parsing headers [`#449`](https://github.com/JakeChampion/fetch/pull/449) 276 | - Add test for cloning GET request [`#440`](https://github.com/JakeChampion/fetch/issues/440) 277 | - Detect broken URL support in PhantomJS and skip test [`b285e61`](https://github.com/JakeChampion/fetch/commit/b285e61fbc4dc21d4b5f7a498046bdff585abf1b) 278 | - Remove secrets [`9240ef4`](https://github.com/JakeChampion/fetch/commit/9240ef453a1ebc3670b8377f9deb771d684e7f68) 279 | - fetch 2.0.2 [`b337f95`](https://github.com/JakeChampion/fetch/commit/b337f9578fa8e21fa5c9fe8d6eb74baaa43a1c02) 280 | 281 | ## [v2.0.1](https://github.com/JakeChampion/fetch/compare/v2.0.0...v2.0.1) 282 | 283 | > 17 November 2016 284 | 285 | - Fix misspelling of [ae]ffect [`#432`](https://github.com/JakeChampion/fetch/pull/432) 286 | - Fix reading ArrayBuffer into string on older browsers [`6f8529e`](https://github.com/JakeChampion/fetch/commit/6f8529e4c5ceacc92c97f58a9bc6538879978f3c) 287 | - Only define `arrayBuffer()` if Blob is also supported [`3d3bb0c`](https://github.com/JakeChampion/fetch/commit/3d3bb0ca72172b224e8101c0a5264adc41f53929) 288 | - Display uncaught errors on the test results page [`54ec096`](https://github.com/JakeChampion/fetch/commit/54ec0965c25a9889e5ba597421faf7b0790de026) 289 | 290 | ### [v2.0.0](https://github.com/JakeChampion/fetch/compare/v1.1.1...v2.0.0) 291 | 292 | > 14 November 2016 293 | 294 | - Change Headers multiple value handling for spec compatibility [`#429`](https://github.com/JakeChampion/fetch/pull/429) 295 | - Firefox now implements `Headers.forEach` natively [`468f877`](https://github.com/JakeChampion/fetch/commit/468f877e4447a2b267236f2f8fa4f1492c0dd20b) 296 | - fetch 2.0.0 [`c576d61`](https://github.com/JakeChampion/fetch/commit/c576d61fee39bb34699bbe870460b6120011150a) 297 | 298 | ## [v1.1.1](https://github.com/JakeChampion/fetch/compare/v1.1.0...v1.1.1) 299 | 300 | > 17 November 2016 301 | 302 | - Fix reading ArrayBuffer into string on older browsers [`1ddcadb`](https://github.com/JakeChampion/fetch/commit/1ddcadb2418c4cf0b206857f424a9af58c0ed57f) 303 | - Only define `arrayBuffer()` if Blob is also supported [`c2556f3`](https://github.com/JakeChampion/fetch/commit/c2556f3ed41a238df4ee384fd8e4c404f3971e64) 304 | - fetch 1.1.1 [`f7a5148`](https://github.com/JakeChampion/fetch/commit/f7a514829820fc77c0f884c74cf2d36356a781c0) 305 | 306 | ## [v1.1.0](https://github.com/JakeChampion/fetch/compare/v1.0.0...v1.1.0) 307 | 308 | > 14 November 2016 309 | 310 | - Support ArrayBufferView types as POST body [`#430`](https://github.com/JakeChampion/fetch/pull/430) 311 | - Spec compatibility for Request/Response constructors and cloning [`#428`](https://github.com/JakeChampion/fetch/pull/428) 312 | - Improve Readme [`#427`](https://github.com/JakeChampion/fetch/pull/427) 313 | - Fix grammar [`#408`](https://github.com/JakeChampion/fetch/pull/408) 314 | - Fixed typo in README.md [`#403`](https://github.com/JakeChampion/fetch/pull/403) 315 | - make X-Request-URL header case-insensitive [`#384`](https://github.com/JakeChampion/fetch/pull/384) 316 | - Better error handling with Saucelabs [`#354`](https://github.com/JakeChampion/fetch/pull/354) 317 | - Update Webpack section in README [`#331`](https://github.com/JakeChampion/fetch/pull/331) 318 | - Attach FileReader event handlers before calling its `read*` method [`#353`](https://github.com/JakeChampion/fetch/issues/353) 319 | - Default Response status is 200 OK [`#376`](https://github.com/JakeChampion/fetch/issues/376) 320 | - Support ArrayBuffer in BodyInit [`#350`](https://github.com/JakeChampion/fetch/issues/350) 321 | - Avoid consuming body when cloning [`#308`](https://github.com/JakeChampion/fetch/issues/308) [`#335`](https://github.com/JakeChampion/fetch/issues/335) 322 | - Rework parsing of raw response HTTP headers [`#422`](https://github.com/JakeChampion/fetch/issues/422) 323 | - Allow reusing the same GET Request instance multiple times [`#411`](https://github.com/JakeChampion/fetch/issues/411) 324 | - Always construct a new Headers instance in Response [`#416`](https://github.com/JakeChampion/fetch/issues/416) 325 | - Rework the Installation section [`#415`](https://github.com/JakeChampion/fetch/issues/415) 326 | - More information about cookies [`#393`](https://github.com/JakeChampion/fetch/issues/393) 327 | - It looks like Safari 10 didn't ship with native fetch [`#401`](https://github.com/JakeChampion/fetch/issues/401) 328 | - Reorganize tests with the new "fetch method" suite [`ba7ffda`](https://github.com/JakeChampion/fetch/commit/ba7ffda7b2bf6b9183fbca04120c042babd17f00) 329 | - Share identical tests between Request & Response [`9a04a06`](https://github.com/JakeChampion/fetch/commit/9a04a0667b92dba567746b26b553ab9a329fa94d) 330 | - ArrayBuffer can now be consumed through `blob()`/`text()` [`9a703ba`](https://github.com/JakeChampion/fetch/commit/9a703ba38ff3bddc94c8929c1e8fae5d766462cd) 331 | 332 | ### [v1.0.0](https://github.com/JakeChampion/fetch/compare/v0.11.1...v1.0.0) 333 | 334 | > 28 April 2016 335 | 336 | - refactor Header iterator methods [`#317`](https://github.com/JakeChampion/fetch/pull/317) 337 | - Add ES2015+ example [`#287`](https://github.com/JakeChampion/fetch/pull/287) 338 | - Switch to `mocha-phantomjs-core` and system PhantomJS [`#314`](https://github.com/JakeChampion/fetch/pull/314) 339 | - Reject promise on request timeout [`#306`](https://github.com/JakeChampion/fetch/pull/306) 340 | - Use uppercase methods in README [`#272`](https://github.com/JakeChampion/fetch/pull/272) 341 | - Guard against `xhr.getAllResponseHeaders()` being `null` [`#289`](https://github.com/JakeChampion/fetch/pull/289) 342 | - Add support for URLSearchParams POST body [`#304`](https://github.com/JakeChampion/fetch/pull/304) 343 | - Add Headers iterators [`#295`](https://github.com/JakeChampion/fetch/pull/295) 344 | - fix example [`#282`](https://github.com/JakeChampion/fetch/pull/282) 345 | - Drop IE-specific status codes workarounds and require IE10+ [`#270`](https://github.com/JakeChampion/fetch/pull/270) 346 | - Reject promise on request timeout [`#294`](https://github.com/JakeChampion/fetch/issues/294) 347 | - Make Headers iterable if Symbol is available [`a1b7674`](https://github.com/JakeChampion/fetch/commit/a1b7674b6942d4265ea47b74760a486b2bf5e3da) 348 | - Support URLSearchParams POST body [`d77810a`](https://github.com/JakeChampion/fetch/commit/d77810a15c78bbbaf2defd4ea3af6db21c8d117f) 349 | - Fix formatting [`edb7c73`](https://github.com/JakeChampion/fetch/commit/edb7c7336f53b5c0e08ef0ccb37e43c8d9de778f) 350 | 351 | ## [v0.11.1](https://github.com/JakeChampion/fetch/compare/v0.11.0...v0.11.1) 352 | 353 | > 5 May 2016 354 | 355 | - Reject promise on request timeout [`#294`](https://github.com/JakeChampion/fetch/issues/294) 356 | - Fix formatting [`3fc66ed`](https://github.com/JakeChampion/fetch/commit/3fc66edc4c0f61a60b2debfca276a7a8140aa2c9) 357 | - Fetch 0.11.1 [`7d9a11d`](https://github.com/JakeChampion/fetch/commit/7d9a11deec5c0ea2d453390be647ba52695166f8) 358 | - Guard against `xhr.getAllResponseHeaders()` being `null` [`8deb829`](https://github.com/JakeChampion/fetch/commit/8deb8296681f6ad0990e0af47b99d71f2a1d1701) 359 | 360 | ## [v0.11.0](https://github.com/JakeChampion/fetch/compare/v0.10.1...v0.11.0) 361 | 362 | > 19 January 2016 363 | 364 | - Handle cases where `self` isn't defined [`#253`](https://github.com/JakeChampion/fetch/pull/253) 365 | - Exercise both polyfill and native `fetch` in test suite [`#258`](https://github.com/JakeChampion/fetch/pull/258) 366 | - Make fetch add a `Content-Type` header based on the type of the body. [`1e4a615`](https://github.com/JakeChampion/fetch/commit/1e4a6151e6a1f4e2e792f7faa0a028498a7be973) 367 | - Cleanup in determining implicit content-type [`3b5dc9c`](https://github.com/JakeChampion/fetch/commit/3b5dc9c17f2be9ca1a2e7030dd8209f0b150bc70) 368 | - Render main test suite as root resource of test server [`b043384`](https://github.com/JakeChampion/fetch/commit/b043384e2d7b68b10172a64e5c5b00a593cd41c3) 369 | 370 | ## [v0.10.1](https://github.com/JakeChampion/fetch/compare/v0.10.0...v0.10.1) 371 | 372 | > 2 November 2015 373 | 374 | - Allow making a POST request with an ArrayBuffer body [`#227`](https://github.com/JakeChampion/fetch/pull/227) 375 | - Run Sauce Labs CI for pull requests [`#220`](https://github.com/JakeChampion/fetch/pull/220) 376 | - Streamline Sauce Labs API interactions [`07dc8ae`](https://github.com/JakeChampion/fetch/commit/07dc8ae4cc9a46ad4af35c99a8bdc0b83fbae28b) 377 | - Download and start Sauce Connect manually [`b3885b4`](https://github.com/JakeChampion/fetch/commit/b3885b4eceb4a818e7f8d2d290f89f4d8eaeb0d3) 378 | - Switch to my credentials for npm publish from CI [`e0a4851`](https://github.com/JakeChampion/fetch/commit/e0a48518734aac116f49962c825522ab99da8338) 379 | 380 | ## [v0.10.0](https://github.com/JakeChampion/fetch/compare/v0.9.0...v0.10.0) 381 | 382 | > 12 October 2015 383 | 384 | - Remove moot `version` property from bower.json [`#159`](https://github.com/JakeChampion/fetch/pull/159) 385 | - Use absolute URL in Response.redirect test [`#219`](https://github.com/JakeChampion/fetch/pull/219) 386 | - Support Response.error() and Response.redirect() [`#212`](https://github.com/JakeChampion/fetch/pull/212) 387 | - Reject the Promise returned by fetch() when Request ctor throws [`#217`](https://github.com/JakeChampion/fetch/pull/217) 388 | - Fix incorrect assertion [`#216`](https://github.com/JakeChampion/fetch/pull/216) 389 | - Remove superfluous assignment [`#213`](https://github.com/JakeChampion/fetch/pull/213) 390 | - Add webpack usage link. [`#195`](https://github.com/JakeChampion/fetch/pull/195) 391 | - Allow passing a Request instance to Request constructor [`#179`](https://github.com/JakeChampion/fetch/pull/179) 392 | - Properly convert undefined/null header values to strings. [`#156`](https://github.com/JakeChampion/fetch/pull/156) 393 | - Code of Conduct [`#174`](https://github.com/JakeChampion/fetch/pull/174) 394 | - Improve documentation for `fetch` caveats [`#164`](https://github.com/JakeChampion/fetch/pull/164) 395 | - Opt into new Travis infrastructure [`#158`](https://github.com/JakeChampion/fetch/pull/158) 396 | - Merge branch 'orphan-black' [`#209`](https://github.com/JakeChampion/fetch/issues/209) [`#185`](https://github.com/JakeChampion/fetch/issues/185) 397 | - Add include credentials example. [`#205`](https://github.com/JakeChampion/fetch/issues/205) 398 | - Add `Request.clone()` and `Response.clone()` methods [`46705f7`](https://github.com/JakeChampion/fetch/commit/46705f798e1c6e6c9ef03156a8ec8f64c9971d69) 399 | - Fix and simplify `Request.clone()` [`fd362dd`](https://github.com/JakeChampion/fetch/commit/fd362ddb1dcb6918cf8203c99f40d7fed019afc4) 400 | - Expand caveats with notes about cookies [`184b647`](https://github.com/JakeChampion/fetch/commit/184b64719f90ba0cc88c6eeb43c9a2a7ea5fb726) 401 | 402 | ## [v0.9.0](https://github.com/JakeChampion/fetch/compare/v0.8.2...v0.9.0) 403 | 404 | > 29 May 2015 405 | 406 | - Implement Headers#forEach correctly [`#150`](https://github.com/JakeChampion/fetch/pull/150) 407 | - Test forEach. [`2f442ce`](https://github.com/JakeChampion/fetch/commit/2f442cebf84f3e7057e2d94408a1b9ec4643c783) 408 | - Fix forEach parameters. [`0449483`](https://github.com/JakeChampion/fetch/commit/0449483b4ab1e9184e74302e1c6fb17e9cd44a75) 409 | - Accept a thisArg forEach parameter. [`bd2fe03`](https://github.com/JakeChampion/fetch/commit/bd2fe03140cfdaf4bd38ca5b4798c775a58b6fd5) 410 | 411 | ## [v0.8.2](https://github.com/JakeChampion/fetch/compare/v0.8.1...v0.8.2) 412 | 413 | > 19 May 2015 414 | 415 | - Set xhr.withCredentials after xhr.open called. [`a847967`](https://github.com/JakeChampion/fetch/commit/a847967a0314a574dada2c31e1825f75ed6dc24a) 416 | - Only support standard options. [`cc9f4b0`](https://github.com/JakeChampion/fetch/commit/cc9f4b0e3e2aaa8cf751dfc2098e58a94fc71e59) 417 | - Fetch 0.8.2 [`0b3e1d7`](https://github.com/JakeChampion/fetch/commit/0b3e1d7c41c75359a3e0b771741ebc2a8823da38) 418 | 419 | ## [v0.8.1](https://github.com/JakeChampion/fetch/compare/v0.8.0...v0.8.1) 420 | 421 | > 4 May 2015 422 | 423 | - Fetch 0.8.1 [`09c316d`](https://github.com/JakeChampion/fetch/commit/09c316d2450c08fde129336438b3a44de4e8177c) 424 | - Ignore script/ dir [`2e39db1`](https://github.com/JakeChampion/fetch/commit/2e39db1b02c5453ed9c3e156f5d68240f0e76907) 425 | 426 | ## [v0.8.0](https://github.com/JakeChampion/fetch/compare/v0.7.0...v0.8.0) 427 | 428 | > 4 May 2015 429 | 430 | - only define _initBody once [`#136`](https://github.com/JakeChampion/fetch/pull/136) 431 | - remove un-needed promise allocations in example [`#120`](https://github.com/JakeChampion/fetch/pull/120) 432 | - Headers constructor in Response constructor [`#107`](https://github.com/JakeChampion/fetch/pull/107) 433 | - Sauce: IE9 [`#102`](https://github.com/JakeChampion/fetch/pull/102) 434 | - Sauce Labs: IE 11 [`#101`](https://github.com/JakeChampion/fetch/pull/101) 435 | - Sauce Labs [`#99`](https://github.com/JakeChampion/fetch/pull/99) 436 | - Add a convenience `ok` getter on `Response` [`#82`](https://github.com/JakeChampion/fetch/pull/82) 437 | - Follow spec on Headers to throw TypeError, add tests for Headers [`#85`](https://github.com/JakeChampion/fetch/pull/85) 438 | - adds .npmignore [`#84`](https://github.com/JakeChampion/fetch/pull/84) 439 | - node.js module link [`#81`](https://github.com/JakeChampion/fetch/pull/81) 440 | - Add script runner for saucelabs [`47fc7d5`](https://github.com/JakeChampion/fetch/commit/47fc7d5a8431505af8dec8326e5d081219ad7d6a) 441 | - Split app and server [`29cc5dc`](https://github.com/JakeChampion/fetch/commit/29cc5dc74441679c63e99145ba841f8abd29da17) 442 | - More scripty [`ba1214a`](https://github.com/JakeChampion/fetch/commit/ba1214acaf766eba9e0a268de495d8b9c9e295c1) 443 | 444 | ## [v0.7.0](https://github.com/JakeChampion/fetch/compare/v0.6.1...v0.7.0) 445 | 446 | > 24 January 2015 447 | 448 | - Centralise the checks for blob and form data support [`#78`](https://github.com/JakeChampion/fetch/pull/78) 449 | - If cors, with credentials [`#77`](https://github.com/JakeChampion/fetch/pull/77) 450 | - Add metadata for repository, bugs and license [`#67`](https://github.com/JakeChampion/fetch/pull/67) 451 | - Declare deliberate "async=true" on XMLHttpRequest open [`#74`](https://github.com/JakeChampion/fetch/pull/74) 452 | - Fix typo in npm install instructions [`#71`](https://github.com/JakeChampion/fetch/pull/71) 453 | - Improve Request/Response BodyInit consuming [`#70`](https://github.com/JakeChampion/fetch/pull/70) 454 | - Fix up body consuming on request [`fbfa9e3`](https://github.com/JakeChampion/fetch/commit/fbfa9e332039d3d4e4e91da6038729d061455ef1) 455 | - Throw TypeError if body is given for GET or HEAD [`5ce5677`](https://github.com/JakeChampion/fetch/commit/5ce56771da78d341561887df2bb65f78425333c4) 456 | - A few more tests and typo fix. [`614b2aa`](https://github.com/JakeChampion/fetch/commit/614b2aab10525f8e2a55124fdb33b374b61a0c87) 457 | 458 | ## [v0.6.1](https://github.com/JakeChampion/fetch/compare/v0.6.0...v0.6.1) 459 | 460 | > 15 January 2015 461 | 462 | - Add charset content-type tests [`7474e42`](https://github.com/JakeChampion/fetch/commit/7474e42af467bcd843f97a3def92b0c7d63e4f48) 463 | - Add additional body init and consume test coverage [`9d58648`](https://github.com/JakeChampion/fetch/commit/9d586486e50a79551b1d12178b3408d1fd57cb35) 464 | - Fix X-Request-URL on CORS requests [`4525329`](https://github.com/JakeChampion/fetch/commit/4525329eb075da74fd7585d4ea8ddeabc97b17a4) 465 | 466 | ## [v0.6.0](https://github.com/JakeChampion/fetch/compare/v0.5.0...v0.6.0) 467 | 468 | > 12 January 2015 469 | 470 | - Suspect this api key was wrong [`#63`](https://github.com/JakeChampion/fetch/pull/63) 471 | - Use responseText on IE9 which lacks XHR2 support [`eeb53d3`](https://github.com/JakeChampion/fetch/commit/eeb53d391dcb12a2d77765bf602fc45427112687) 472 | - Tidy up binary file reader [`7436589`](https://github.com/JakeChampion/fetch/commit/74365897619b533fe7b9080568ad43e852130974) 473 | - Use `xhr.responseType = 'blob'` to preserve binary data. [`080358d`](https://github.com/JakeChampion/fetch/commit/080358ddb26ff37cfd27caf730af9cd3c184bc42) 474 | 475 | ## [v0.5.0](https://github.com/JakeChampion/fetch/compare/v0.4.0...v0.5.0) 476 | 477 | > 12 January 2015 478 | 479 | - Enable travis to publish to npm. [`#57`](https://github.com/JakeChampion/fetch/pull/57) 480 | - Make Headers case insensitive though lowercasing. [`#62`](https://github.com/JakeChampion/fetch/pull/62) 481 | - Support credentials [`#56`](https://github.com/JakeChampion/fetch/pull/56) 482 | - Switch to Mocha [`#59`](https://github.com/JakeChampion/fetch/pull/59) 483 | - Test Atomic HTTP redirect handling [`#55`](https://github.com/JakeChampion/fetch/pull/55) 484 | - Mark FormData support as optional [`#54`](https://github.com/JakeChampion/fetch/pull/54) 485 | - Add promise test helper [`#53`](https://github.com/JakeChampion/fetch/pull/53) 486 | - Test in web worker [`#51`](https://github.com/JakeChampion/fetch/issues/51) 487 | - Group tests [`ecd8600`](https://github.com/JakeChampion/fetch/commit/ecd8600932b0d8495d646df0d6fa74874cd57713) 488 | - Switch to mocha [`cbd6c66`](https://github.com/JakeChampion/fetch/commit/cbd6c66fe4bbda1b63cef54c299c2081c4b50955) 489 | - Skip tests in phantomjs [`8a4b620`](https://github.com/JakeChampion/fetch/commit/8a4b62027ea7c590861364e853eccb5f52a8991b) 490 | 491 | ## [v0.4.0](https://github.com/JakeChampion/fetch/compare/v0.3.2...v0.4.0) 492 | 493 | > 29 December 2014 494 | 495 | - Assign to self [`#52`](https://github.com/JakeChampion/fetch/pull/52) 496 | - Web Workers support [`#48`](https://github.com/JakeChampion/fetch/pull/48) 497 | - Align used flag error message to Chrome's implementation [`#44`](https://github.com/JakeChampion/fetch/pull/44) 498 | - Add missing quote. [`#40`](https://github.com/JakeChampion/fetch/issues/40) 499 | - Align bodyUsed error message to Chrome's implementation [`e414284`](https://github.com/JakeChampion/fetch/commit/e4142843fab42705cc65d526bf86b1662da7f338) 500 | - Avoid testing implementation specific error messages [`cc42153`](https://github.com/JakeChampion/fetch/commit/cc4215367d2298f779ee9a5ab3f8c9cb6207c8c5) 501 | - Set esnext option [`3ebc441`](https://github.com/JakeChampion/fetch/commit/3ebc44129997dbc2e331450b6e876b8f6e36437b) 502 | 503 | ## [v0.3.2](https://github.com/JakeChampion/fetch/compare/v0.3.1...v0.3.2) 504 | 505 | > 24 November 2014 506 | 507 | - FormData should only able to consume once [`#38`](https://github.com/JakeChampion/fetch/pull/38) 508 | - Test formData body consumption. [`4a7e655`](https://github.com/JakeChampion/fetch/commit/4a7e655b4f0361524d16adb16148c1fe0f2f1f0f) 509 | - Fetch 0.3.2 [`830231e`](https://github.com/JakeChampion/fetch/commit/830231e5682175fe04088b291192f72c59aed998) 510 | 511 | ## [v0.3.1](https://github.com/JakeChampion/fetch/compare/v0.3.0...v0.3.1) 512 | 513 | > 21 November 2014 514 | 515 | - Reject promise with TypeError for network failures. [`#36`](https://github.com/JakeChampion/fetch/pull/36) 516 | - Reject example promise with an Error. [`#35`](https://github.com/JakeChampion/fetch/issues/35) 517 | - Fetch 0.3.1 [`eb3f9b2`](https://github.com/JakeChampion/fetch/commit/eb3f9b2b1fa7804883cbf853102944847d65e204) 518 | 519 | ## [v0.3.0](https://github.com/JakeChampion/fetch/compare/v0.2.1...v0.3.0) 520 | 521 | > 13 November 2014 522 | 523 | - IE 9+ fixes [`#28`](https://github.com/JakeChampion/fetch/pull/28) 524 | - Move body to _body to prevent direct access [`#32`](https://github.com/JakeChampion/fetch/pull/32) 525 | - Remove form encoded object body. [`#30`](https://github.com/JakeChampion/fetch/pull/30) 526 | - Document how to use in Browserify… [`#29`](https://github.com/JakeChampion/fetch/pull/29) 527 | - Auto-detect available port when running headless tests [`#27`](https://github.com/JakeChampion/fetch/pull/27) 528 | - Shell highlight [`#24`](https://github.com/JakeChampion/fetch/pull/24) 529 | - use shorthand npm installation [`#23`](https://github.com/JakeChampion/fetch/pull/23) 530 | - Add name/version/main so it can be installed from npm. [`#22`](https://github.com/JakeChampion/fetch/pull/22) 531 | - Add example of success and error handlers [`#18`](https://github.com/JakeChampion/fetch/pull/18) 532 | - Test Server [`#13`](https://github.com/JakeChampion/fetch/pull/13) 533 | - Travis [`#12`](https://github.com/JakeChampion/fetch/pull/12) 534 | - Add test server [`3316bda`](https://github.com/JakeChampion/fetch/commit/3316bdaf03e4ef2eef35161913efca9a8bd31457) 535 | - Uppercase the HTTP method name [`c71f1dd`](https://github.com/JakeChampion/fetch/commit/c71f1dd9bb0a84453ad2e14cb4a638cc735e1344) 536 | - Skip blob tests on phantom [`c02cad2`](https://github.com/JakeChampion/fetch/commit/c02cad221e0d239dd79e42f2d91c9bb48501776c) 537 | 538 | ## [v0.2.1](https://github.com/JakeChampion/fetch/compare/v0.2.0...v0.2.1) 539 | 540 | > 15 October 2014 541 | 542 | - Use of `Promise.reject` as a constructor [`#10`](https://github.com/JakeChampion/fetch/pull/10) 543 | - Fixed uncaught error when a body was consumed more than once. [`e428559`](https://github.com/JakeChampion/fetch/commit/e428559a68f5c9d445bf2fb5a91fb3f7e35b2f5d) 544 | - Fetch 0.2.1 [`8160180`](https://github.com/JakeChampion/fetch/commit/81601803ec9fd1ffa29f4d527b12e586dd9840c1) 545 | 546 | ## [v0.2.0](https://github.com/JakeChampion/fetch/compare/v0.1.0...v0.2.0) 547 | 548 | > 15 October 2014 549 | 550 | - Parse form encoded response body [`#8`](https://github.com/JakeChampion/fetch/pull/8) 551 | - Allow body to be consumed only once [`#7`](https://github.com/JakeChampion/fetch/pull/7) 552 | - throw proper errors [`#5`](https://github.com/JakeChampion/fetch/pull/5) 553 | - Allow body to be consumed a single time. [`c7a27dc`](https://github.com/JakeChampion/fetch/commit/c7a27dc12f43f398f8159db98f3745d8c3320515) 554 | - Parse form encoded response body. [`60271ce`](https://github.com/JakeChampion/fetch/commit/60271cef8aa13641ba99c197f974fbb4a5f77c57) 555 | - Extract consumed function. [`a709976`](https://github.com/JakeChampion/fetch/commit/a7099768059b9befbb656c6b1e3944bad8a97327) 556 | 557 | ## v0.1.0 558 | 559 | > 13 October 2014 560 | 561 | - 🐶 [`c1af6de`](https://github.com/JakeChampion/fetch/commit/c1af6de1e87d2753203f2e750e48b625fac2e24a) 562 | - Start tests. [`44e796a`](https://github.com/JakeChampion/fetch/commit/44e796a874dd88bfd365cc6dcddfe973daeec08a) 563 | - Add readme. [`7eee89d`](https://github.com/JakeChampion/fetch/commit/7eee89d15ee21e762a04b4c773fcc3d7d50a13f7) 564 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource+fetch@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to our `fetch` polyfill! 4 | 5 | Note that we only accept features that are also described in the official [fetch 6 | specification][]. However, the aim of this project is not to implement the 7 | complete specification; just the parts that are feasible to emulate using 8 | XMLHttpRequest. See [Caveats][] for some examples of features that we are 9 | unlikely to implement. 10 | 11 | Contributions to this project are [released][tos] to the public under the 12 | [project's open source license](LICENSE). 13 | 14 | ## Running tests 15 | 16 | Running `npm test` will: 17 | 18 | 1. Build the `dist/` files; 19 | 1. Run the test suite in headless Chrome & Firefox; 20 | 1. Run the same test suite in Web Worker mode. 21 | 22 | When editing tests or implementation, keep `npm run karma` running: 23 | 24 | - You can connect additional browsers by navigating to `http://localhost:9876/`; 25 | - Changes to [test.js](test/test.js) will automatically re-run the tests in all 26 | connected browsers; 27 | - When changing [fetch.js](fetch.js), re-run tests by executing `make`; 28 | - Re-run specific tests with `./node_modules/.bin/karma run -- --grep=`. 29 | 30 | ## Submitting a pull request 31 | 32 | 1. [Fork][fork] and clone the repository; 33 | 1. Create a new branch: `git checkout -b my-branch-name`; 34 | 1. Make your change, push to your fork and [submit a pull request][pr]; 35 | 1. Pat your self on the back and wait for your pull request to be reviewed. 36 | 37 | Here are a few things you can do that will increase the likelihood of your pull 38 | request being accepted: 39 | 40 | - Keep your change as focused as possible. If there are multiple changes you 41 | would like to make that are not dependent upon each other, consider submitting 42 | them as separate pull requests. 43 | - Write a [good commit message][]. 44 | 45 | ## Resources 46 | 47 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 48 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 49 | - [GitHub Help](https://help.github.com) 50 | 51 | 52 | [fetch specification]: https://fetch.spec.whatwg.org 53 | [tos]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license 54 | [fork]: https://github.com/github/fetch/fork 55 | [pr]: https://github.com/github/fetch/compare 56 | [good commit message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 57 | [caveats]: https://github.github.io/fetch/#caveats 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2023 GitHub, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: lint dist/fetch.umd.js 2 | 3 | lint: node_modules/ 4 | ./node_modules/.bin/eslint --report-unused-disable-directives *.js test/*.js 5 | 6 | dist/fetch.umd.js: fetch.js rollup.config.js node_modules/ 7 | ./node_modules/.bin/rollup -c 8 | 9 | dist/fetch.umd.js.flow: fetch.js.flow 10 | cp $< $@ 11 | 12 | node_modules/: 13 | npm install 14 | 15 | clean: 16 | rm -rf ./bower_components ./node_modules ./dist 17 | 18 | .PHONY: clean lint test make dist/fetch.umd.js dist/fetch.umd.js.flow 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # window.fetch polyfill 2 | 3 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/JakeChampion/fetch/badge)](https://securityscorecards.dev/viewer/?uri=github.com/JakeChampion/fetch) 4 | 5 | The `fetch()` function is a Promise-based mechanism for programmatically making 6 | web requests in the browser. This project is a polyfill that implements a subset 7 | of the standard [Fetch specification][], enough to make `fetch` a viable 8 | replacement for most uses of XMLHttpRequest in traditional web applications. 9 | 10 | ## Table of Contents 11 | 12 | * [Read this first](#read-this-first) 13 | * [Installation](#installation) 14 | * [Usage](#usage) 15 | * [Importing](#importing) 16 | * [HTML](#html) 17 | * [JSON](#json) 18 | * [Response metadata](#response-metadata) 19 | * [Post form](#post-form) 20 | * [Post JSON](#post-json) 21 | * [File upload](#file-upload) 22 | * [Caveats](#caveats) 23 | * [Handling HTTP error statuses](#handling-http-error-statuses) 24 | * [Sending cookies](#sending-cookies) 25 | * [Receiving cookies](#receiving-cookies) 26 | * [Redirect modes](#redirect-modes) 27 | * [Obtaining the Response URL](#obtaining-the-response-url) 28 | * [Aborting requests](#aborting-requests) 29 | * [Browser Support](#browser-support) 30 | 31 | ## Read this first 32 | 33 | * If you believe you found a bug with how `fetch` behaves in your browser, 34 | please **don't open an issue in this repository** unless you are testing in 35 | an old version of a browser that doesn't support `window.fetch` natively. 36 | Make sure you read this _entire_ readme, especially the [Caveats](#caveats) 37 | section, as there's probably a known work-around for an issue you've found. 38 | This project is a _polyfill_, and since all modern browsers now implement the 39 | `fetch` function natively, **no code from this project** actually takes any 40 | effect there. See [Browser support](#browser-support) for detailed 41 | information. 42 | 43 | * If you have trouble **making a request to another domain** (a different 44 | subdomain or port number also constitutes another domain), please familiarize 45 | yourself with all the intricacies and limitations of [CORS][] requests. 46 | Because CORS requires participation of the server by implementing specific 47 | HTTP response headers, it is often nontrivial to set up or debug. CORS is 48 | exclusively handled by the browser's internal mechanisms which this polyfill 49 | cannot influence. 50 | 51 | * This project **doesn't work under Node.js environments**. It's meant for web 52 | browsers only. You should ensure that your application doesn't try to package 53 | and run this on the server. 54 | 55 | * If you have an idea for a new feature of `fetch`, **submit your feature 56 | requests** to the [specification's repository](https://github.com/whatwg/fetch/issues). 57 | We only add features and APIs that are part of the [Fetch specification][]. 58 | 59 | ## Installation 60 | 61 | ``` 62 | npm install whatwg-fetch --save 63 | ``` 64 | 65 | You will also need a Promise polyfill for [older browsers](https://caniuse.com/promises). 66 | We recommend [taylorhakes/promise-polyfill](https://github.com/taylorhakes/promise-polyfill) 67 | for its small size and Promises/A+ compatibility. 68 | 69 | ## Usage 70 | 71 | ### Importing 72 | 73 | Importing will automatically polyfill `window.fetch` and related APIs: 74 | 75 | ```javascript 76 | import 'whatwg-fetch' 77 | 78 | window.fetch(...) 79 | ``` 80 | 81 | If for some reason you need to access the polyfill implementation, it is 82 | available via exports: 83 | 84 | ```javascript 85 | import {fetch as fetchPolyfill} from 'whatwg-fetch' 86 | 87 | window.fetch(...) // use native browser version 88 | fetchPolyfill(...) // use polyfill implementation 89 | ``` 90 | 91 | This approach can be used to, for example, use [abort 92 | functionality](#aborting-requests) in browsers that implement a native but 93 | outdated version of fetch that doesn't support aborting. 94 | 95 | For use with webpack, add this package in the `entry` configuration option 96 | before your application entry point: 97 | 98 | ```javascript 99 | entry: ['whatwg-fetch', ...] 100 | ``` 101 | 102 | ### HTML 103 | 104 | ```javascript 105 | fetch('/users.html') 106 | .then(function(response) { 107 | return response.text() 108 | }).then(function(body) { 109 | document.body.innerHTML = body 110 | }) 111 | ``` 112 | 113 | ### JSON 114 | 115 | ```javascript 116 | fetch('/users.json') 117 | .then(function(response) { 118 | return response.json() 119 | }).then(function(json) { 120 | console.log('parsed json', json) 121 | }).catch(function(ex) { 122 | console.log('parsing failed', ex) 123 | }) 124 | ``` 125 | 126 | ### Response metadata 127 | 128 | ```javascript 129 | fetch('/users.json').then(function(response) { 130 | console.log(response.headers.get('Content-Type')) 131 | console.log(response.headers.get('Date')) 132 | console.log(response.status) 133 | console.log(response.statusText) 134 | }) 135 | ``` 136 | 137 | ### Post form 138 | 139 | ```javascript 140 | var form = document.querySelector('form') 141 | 142 | fetch('/users', { 143 | method: 'POST', 144 | body: new FormData(form) 145 | }) 146 | ``` 147 | 148 | ### Post JSON 149 | 150 | ```javascript 151 | fetch('/users', { 152 | method: 'POST', 153 | headers: { 154 | 'Content-Type': 'application/json' 155 | }, 156 | body: JSON.stringify({ 157 | name: 'Hubot', 158 | login: 'hubot', 159 | }) 160 | }) 161 | ``` 162 | 163 | ### File upload 164 | 165 | ```javascript 166 | var input = document.querySelector('input[type="file"]') 167 | 168 | var data = new FormData() 169 | data.append('file', input.files[0]) 170 | data.append('user', 'hubot') 171 | 172 | fetch('/avatars', { 173 | method: 'POST', 174 | body: data 175 | }) 176 | ``` 177 | 178 | ### Caveats 179 | 180 | * The Promise returned from `fetch()` **won't reject on HTTP error status** 181 | even if the response is an HTTP 404 or 500. Instead, it will resolve normally, 182 | and it will only reject on network failure or if anything prevented the 183 | request from completing. 184 | 185 | * For maximum browser compatibility when it comes to sending & receiving 186 | cookies, always supply the `credentials: 'same-origin'` option instead of 187 | relying on the default. See [Sending cookies](#sending-cookies). 188 | 189 | * Not all Fetch standard options are supported in this polyfill. For instance, 190 | [`redirect`](#redirect-modes) and 191 | `cache` directives are ignored. 192 | 193 | * `keepalive` is not supported because it would involve making a synchronous XHR, which is something this project is not willing to do. See [issue #700](https://github.com/github/fetch/issues/700#issuecomment-484188326) for more information. 194 | 195 | #### Handling HTTP error statuses 196 | 197 | To have `fetch` Promise reject on HTTP error statuses, i.e. on any non-2xx 198 | status, define a custom response handler: 199 | 200 | ```javascript 201 | function checkStatus(response) { 202 | if (response.status >= 200 && response.status < 300) { 203 | return response 204 | } else { 205 | var error = new Error(response.statusText) 206 | error.response = response 207 | throw error 208 | } 209 | } 210 | 211 | function parseJSON(response) { 212 | return response.json() 213 | } 214 | 215 | fetch('/users') 216 | .then(checkStatus) 217 | .then(parseJSON) 218 | .then(function(data) { 219 | console.log('request succeeded with JSON response', data) 220 | }).catch(function(error) { 221 | console.log('request failed', error) 222 | }) 223 | ``` 224 | 225 | #### Sending cookies 226 | 227 | For [CORS][] requests, use `credentials: 'include'` to allow sending credentials 228 | to other domains: 229 | 230 | ```javascript 231 | fetch('https://example.com:1234/users', { 232 | credentials: 'include' 233 | }) 234 | ``` 235 | 236 | The default value for `credentials` is "same-origin". 237 | 238 | The default for `credentials` wasn't always the same, though. The following 239 | versions of browsers implemented an older version of the fetch specification 240 | where the default was "omit": 241 | 242 | * Firefox 39-60 243 | * Chrome 42-67 244 | * Safari 10.1-11.1.2 245 | 246 | If you target these browsers, it's advisable to always specify `credentials: 247 | 'same-origin'` explicitly with all fetch requests instead of relying on the 248 | default: 249 | 250 | ```javascript 251 | fetch('/users', { 252 | credentials: 'same-origin' 253 | }) 254 | ``` 255 | 256 | Note: due to [limitations of 257 | XMLHttpRequest](https://github.com/github/fetch/pull/56#issuecomment-68835992), 258 | using `credentials: 'omit'` is not respected for same domains in browsers where 259 | this polyfill is active. Cookies will always be sent to same domains in older 260 | browsers. 261 | 262 | #### Receiving cookies 263 | 264 | As with XMLHttpRequest, the `Set-Cookie` response header returned from the 265 | server is a [forbidden header name][] and therefore can't be programmatically 266 | read with `response.headers.get()`. Instead, it's the browser's responsibility 267 | to handle new cookies being set (if applicable to the current URL). Unless they 268 | are HTTP-only, new cookies will be available through `document.cookie`. 269 | 270 | #### Redirect modes 271 | 272 | The Fetch specification defines these values for [the `redirect` 273 | option](https://fetch.spec.whatwg.org/#concept-request-redirect-mode): "follow" 274 | (the default), "error", and "manual". 275 | 276 | Due to limitations of XMLHttpRequest, only the "follow" mode is available in 277 | browsers where this polyfill is active. 278 | 279 | #### Obtaining the Response URL 280 | 281 | Due to limitations of XMLHttpRequest, the `response.url` value might not be 282 | reliable after HTTP redirects on older browsers. 283 | 284 | The solution is to configure the server to set the response HTTP header 285 | `X-Request-URL` to the current URL after any redirect that might have happened. 286 | It should be safe to set it unconditionally. 287 | 288 | ``` ruby 289 | # Ruby on Rails controller example 290 | response.headers['X-Request-URL'] = request.url 291 | ``` 292 | 293 | This server workaround is necessary if you need reliable `response.url` in 294 | Firefox < 32, Chrome < 37, Safari, or IE. 295 | 296 | #### Aborting requests 297 | 298 | This polyfill supports 299 | [the abortable fetch API](https://developers.google.com/web/updates/2017/09/abortable-fetch). 300 | However, aborting a fetch requires use of two additional DOM APIs: 301 | [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and 302 | [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). 303 | Typically, browsers that do not support fetch will also not support 304 | AbortController or AbortSignal. Consequently, you will need to include 305 | [an additional polyfill](https://www.npmjs.com/package/yet-another-abortcontroller-polyfill) 306 | for these APIs to abort fetches: 307 | 308 | ```js 309 | import 'yet-another-abortcontroller-polyfill' 310 | import {fetch} from 'whatwg-fetch' 311 | 312 | // use native browser implementation if it supports aborting 313 | const abortableFetch = ('signal' in new Request('')) ? window.fetch : fetch 314 | 315 | const controller = new AbortController() 316 | 317 | abortableFetch('/avatars', { 318 | signal: controller.signal 319 | }).catch(function(ex) { 320 | if (ex.name === 'AbortError') { 321 | console.log('request aborted') 322 | } 323 | }) 324 | 325 | // some time later... 326 | controller.abort() 327 | ``` 328 | 329 | ## Browser Support 330 | 331 | - Chrome 332 | - Firefox 333 | - Safari 6.1+ 334 | - Internet Explorer 10+ 335 | 336 | Note: modern browsers such as Chrome, Firefox, Microsoft Edge, and Safari contain native 337 | implementations of `window.fetch`, therefore the code from this polyfill doesn't 338 | have any effect on those browsers. If you believe you've encountered an error 339 | with how `window.fetch` is implemented in any of these browsers, you should file 340 | an issue with that browser vendor instead of this project. 341 | 342 | 343 | [fetch specification]: https://fetch.spec.whatwg.org 344 | [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 345 | "Cross-origin resource sharing" 346 | [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet 347 | "Cross-site request forgery" 348 | [forbidden header name]: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name 349 | [releases]: https://github.com/github/fetch/releases 350 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are applied only to the latest release. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. 10 | Please disclose it at [security advisory](https://github.com/JakeChampion/fetch/security/advisories/new). 11 | This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 90 days to work on a fix before public exposure. 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch", 3 | "main": "fetch.js", 4 | "ignore": [ 5 | ".*", 6 | "*.md", 7 | "examples/", 8 | "Makefile", 9 | "package.json", 10 | "script/", 11 | "test/" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-prototype-builtins */ 2 | var g = 3 | (typeof globalThis !== 'undefined' && globalThis) || 4 | (typeof self !== 'undefined' && self) || 5 | // eslint-disable-next-line no-undef 6 | (typeof global !== 'undefined' && global) || 7 | {} 8 | 9 | var support = { 10 | searchParams: 'URLSearchParams' in g, 11 | iterable: 'Symbol' in g && 'iterator' in Symbol, 12 | blob: 13 | 'FileReader' in g && 14 | 'Blob' in g && 15 | (function() { 16 | try { 17 | new Blob() 18 | return true 19 | } catch (e) { 20 | return false 21 | } 22 | })(), 23 | formData: 'FormData' in g, 24 | arrayBuffer: 'ArrayBuffer' in g 25 | } 26 | 27 | function isDataView(obj) { 28 | return obj && DataView.prototype.isPrototypeOf(obj) 29 | } 30 | 31 | if (support.arrayBuffer) { 32 | var viewClasses = [ 33 | '[object Int8Array]', 34 | '[object Uint8Array]', 35 | '[object Uint8ClampedArray]', 36 | '[object Int16Array]', 37 | '[object Uint16Array]', 38 | '[object Int32Array]', 39 | '[object Uint32Array]', 40 | '[object Float32Array]', 41 | '[object Float64Array]' 42 | ] 43 | 44 | var isArrayBufferView = 45 | ArrayBuffer.isView || 46 | function(obj) { 47 | return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 48 | } 49 | } 50 | 51 | function normalizeName(name) { 52 | if (typeof name !== 'string') { 53 | name = String(name) 54 | } 55 | if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { 56 | throw new TypeError('Invalid character in header field name: "' + name + '"') 57 | } 58 | return name.toLowerCase() 59 | } 60 | 61 | function normalizeValue(value) { 62 | if (typeof value !== 'string') { 63 | value = String(value) 64 | } 65 | return value 66 | } 67 | 68 | // Build a destructive iterator for the value list 69 | function iteratorFor(items) { 70 | var iterator = { 71 | next: function() { 72 | var value = items.shift() 73 | return {done: value === undefined, value: value} 74 | } 75 | } 76 | 77 | if (support.iterable) { 78 | iterator[Symbol.iterator] = function() { 79 | return iterator 80 | } 81 | } 82 | 83 | return iterator 84 | } 85 | 86 | export function Headers(headers) { 87 | this.map = {} 88 | 89 | if (headers instanceof Headers) { 90 | headers.forEach(function(value, name) { 91 | this.append(name, value) 92 | }, this) 93 | } else if (Array.isArray(headers)) { 94 | headers.forEach(function(header) { 95 | if (header.length != 2) { 96 | throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) 97 | } 98 | this.append(header[0], header[1]) 99 | }, this) 100 | } else if (headers) { 101 | Object.getOwnPropertyNames(headers).forEach(function(name) { 102 | this.append(name, headers[name]) 103 | }, this) 104 | } 105 | } 106 | 107 | Headers.prototype.append = function(name, value) { 108 | name = normalizeName(name) 109 | value = normalizeValue(value) 110 | var oldValue = this.map[name] 111 | this.map[name] = oldValue ? oldValue + ', ' + value : value 112 | } 113 | 114 | Headers.prototype['delete'] = function(name) { 115 | delete this.map[normalizeName(name)] 116 | } 117 | 118 | Headers.prototype.get = function(name) { 119 | name = normalizeName(name) 120 | return this.has(name) ? this.map[name] : null 121 | } 122 | 123 | Headers.prototype.has = function(name) { 124 | return this.map.hasOwnProperty(normalizeName(name)) 125 | } 126 | 127 | Headers.prototype.set = function(name, value) { 128 | this.map[normalizeName(name)] = normalizeValue(value) 129 | } 130 | 131 | Headers.prototype.forEach = function(callback, thisArg) { 132 | for (var name in this.map) { 133 | if (this.map.hasOwnProperty(name)) { 134 | callback.call(thisArg, this.map[name], name, this) 135 | } 136 | } 137 | } 138 | 139 | Headers.prototype.keys = function() { 140 | var items = [] 141 | this.forEach(function(value, name) { 142 | items.push(name) 143 | }) 144 | return iteratorFor(items) 145 | } 146 | 147 | Headers.prototype.values = function() { 148 | var items = [] 149 | this.forEach(function(value) { 150 | items.push(value) 151 | }) 152 | return iteratorFor(items) 153 | } 154 | 155 | Headers.prototype.entries = function() { 156 | var items = [] 157 | this.forEach(function(value, name) { 158 | items.push([name, value]) 159 | }) 160 | return iteratorFor(items) 161 | } 162 | 163 | if (support.iterable) { 164 | Headers.prototype[Symbol.iterator] = Headers.prototype.entries 165 | } 166 | 167 | function consumed(body) { 168 | if (body._noBody) return 169 | if (body.bodyUsed) { 170 | return Promise.reject(new TypeError('Already read')) 171 | } 172 | body.bodyUsed = true 173 | } 174 | 175 | function fileReaderReady(reader) { 176 | return new Promise(function(resolve, reject) { 177 | reader.onload = function() { 178 | resolve(reader.result) 179 | } 180 | reader.onerror = function() { 181 | reject(reader.error) 182 | } 183 | }) 184 | } 185 | 186 | function readBlobAsArrayBuffer(blob) { 187 | var reader = new FileReader() 188 | var promise = fileReaderReady(reader) 189 | reader.readAsArrayBuffer(blob) 190 | return promise 191 | } 192 | 193 | function readBlobAsText(blob) { 194 | var reader = new FileReader() 195 | var promise = fileReaderReady(reader) 196 | var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type) 197 | var encoding = match ? match[1] : 'utf-8' 198 | reader.readAsText(blob, encoding) 199 | return promise 200 | } 201 | 202 | function readArrayBufferAsText(buf) { 203 | var view = new Uint8Array(buf) 204 | var chars = new Array(view.length) 205 | 206 | for (var i = 0; i < view.length; i++) { 207 | chars[i] = String.fromCharCode(view[i]) 208 | } 209 | return chars.join('') 210 | } 211 | 212 | function bufferClone(buf) { 213 | if (buf.slice) { 214 | return buf.slice(0) 215 | } else { 216 | var view = new Uint8Array(buf.byteLength) 217 | view.set(new Uint8Array(buf)) 218 | return view.buffer 219 | } 220 | } 221 | 222 | function Body() { 223 | this.bodyUsed = false 224 | 225 | this._initBody = function(body) { 226 | /* 227 | fetch-mock wraps the Response object in an ES6 Proxy to 228 | provide useful test harness features such as flush. However, on 229 | ES5 browsers without fetch or Proxy support pollyfills must be used; 230 | the proxy-pollyfill is unable to proxy an attribute unless it exists 231 | on the object before the Proxy is created. This change ensures 232 | Response.bodyUsed exists on the instance, while maintaining the 233 | semantic of setting Request.bodyUsed in the constructor before 234 | _initBody is called. 235 | */ 236 | // eslint-disable-next-line no-self-assign 237 | this.bodyUsed = this.bodyUsed 238 | this._bodyInit = body 239 | if (!body) { 240 | this._noBody = true; 241 | this._bodyText = '' 242 | } else if (typeof body === 'string') { 243 | this._bodyText = body 244 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { 245 | this._bodyBlob = body 246 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { 247 | this._bodyFormData = body 248 | } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { 249 | this._bodyText = body.toString() 250 | } else if (support.arrayBuffer && support.blob && isDataView(body)) { 251 | this._bodyArrayBuffer = bufferClone(body.buffer) 252 | // IE 10-11 can't handle a DataView body. 253 | this._bodyInit = new Blob([this._bodyArrayBuffer]) 254 | } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { 255 | this._bodyArrayBuffer = bufferClone(body) 256 | } else { 257 | this._bodyText = body = Object.prototype.toString.call(body) 258 | } 259 | 260 | if (!this.headers.get('content-type')) { 261 | if (typeof body === 'string') { 262 | this.headers.set('content-type', 'text/plain;charset=UTF-8') 263 | } else if (this._bodyBlob && this._bodyBlob.type) { 264 | this.headers.set('content-type', this._bodyBlob.type) 265 | } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { 266 | this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') 267 | } 268 | } 269 | } 270 | 271 | if (support.blob) { 272 | this.blob = function() { 273 | var rejected = consumed(this) 274 | if (rejected) { 275 | return rejected 276 | } 277 | 278 | if (this._bodyBlob) { 279 | return Promise.resolve(this._bodyBlob) 280 | } else if (this._bodyArrayBuffer) { 281 | return Promise.resolve(new Blob([this._bodyArrayBuffer])) 282 | } else if (this._bodyFormData) { 283 | throw new Error('could not read FormData body as blob') 284 | } else { 285 | return Promise.resolve(new Blob([this._bodyText])) 286 | } 287 | } 288 | } 289 | 290 | this.arrayBuffer = function() { 291 | if (this._bodyArrayBuffer) { 292 | var isConsumed = consumed(this) 293 | if (isConsumed) { 294 | return isConsumed 295 | } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { 296 | return Promise.resolve( 297 | this._bodyArrayBuffer.buffer.slice( 298 | this._bodyArrayBuffer.byteOffset, 299 | this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength 300 | ) 301 | ) 302 | } else { 303 | return Promise.resolve(this._bodyArrayBuffer) 304 | } 305 | } else if (support.blob) { 306 | return this.blob().then(readBlobAsArrayBuffer) 307 | } else { 308 | throw new Error('could not read as ArrayBuffer') 309 | } 310 | } 311 | 312 | this.text = function() { 313 | var rejected = consumed(this) 314 | if (rejected) { 315 | return rejected 316 | } 317 | 318 | if (this._bodyBlob) { 319 | return readBlobAsText(this._bodyBlob) 320 | } else if (this._bodyArrayBuffer) { 321 | return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) 322 | } else if (this._bodyFormData) { 323 | throw new Error('could not read FormData body as text') 324 | } else { 325 | return Promise.resolve(this._bodyText) 326 | } 327 | } 328 | 329 | if (support.formData) { 330 | this.formData = function() { 331 | return this.text().then(decode) 332 | } 333 | } 334 | 335 | this.json = function() { 336 | return this.text().then(JSON.parse) 337 | } 338 | 339 | return this 340 | } 341 | 342 | // HTTP methods whose capitalization should be normalized 343 | var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'] 344 | 345 | function normalizeMethod(method) { 346 | var upcased = method.toUpperCase() 347 | return methods.indexOf(upcased) > -1 ? upcased : method 348 | } 349 | 350 | export function Request(input, options) { 351 | if (!(this instanceof Request)) { 352 | throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') 353 | } 354 | 355 | options = options || {} 356 | var body = options.body 357 | 358 | if (input instanceof Request) { 359 | if (input.bodyUsed) { 360 | throw new TypeError('Already read') 361 | } 362 | this.url = input.url 363 | this.credentials = input.credentials 364 | if (!options.headers) { 365 | this.headers = new Headers(input.headers) 366 | } 367 | this.method = input.method 368 | this.mode = input.mode 369 | this.signal = input.signal 370 | if (!body && input._bodyInit != null) { 371 | body = input._bodyInit 372 | input.bodyUsed = true 373 | } 374 | } else { 375 | this.url = String(input) 376 | } 377 | 378 | this.credentials = options.credentials || this.credentials || 'same-origin' 379 | if (options.headers || !this.headers) { 380 | this.headers = new Headers(options.headers) 381 | } 382 | this.method = normalizeMethod(options.method || this.method || 'GET') 383 | this.mode = options.mode || this.mode || null 384 | this.signal = options.signal || this.signal || (function () { 385 | if ('AbortController' in g) { 386 | var ctrl = new AbortController(); 387 | return ctrl.signal; 388 | } 389 | }()); 390 | this.referrer = null 391 | 392 | if ((this.method === 'GET' || this.method === 'HEAD') && body) { 393 | throw new TypeError('Body not allowed for GET or HEAD requests') 394 | } 395 | this._initBody(body) 396 | 397 | if (this.method === 'GET' || this.method === 'HEAD') { 398 | if (options.cache === 'no-store' || options.cache === 'no-cache') { 399 | // Search for a '_' parameter in the query string 400 | var reParamSearch = /([?&])_=[^&]*/ 401 | if (reParamSearch.test(this.url)) { 402 | // If it already exists then set the value with the current time 403 | this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()) 404 | } else { 405 | // Otherwise add a new '_' parameter to the end with the current time 406 | var reQueryString = /\?/ 407 | this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime() 408 | } 409 | } 410 | } 411 | } 412 | 413 | Request.prototype.clone = function() { 414 | return new Request(this, {body: this._bodyInit}) 415 | } 416 | 417 | function decode(body) { 418 | var form = new FormData() 419 | body 420 | .trim() 421 | .split('&') 422 | .forEach(function(bytes) { 423 | if (bytes) { 424 | var split = bytes.split('=') 425 | var name = split.shift().replace(/\+/g, ' ') 426 | var value = split.join('=').replace(/\+/g, ' ') 427 | form.append(decodeURIComponent(name), decodeURIComponent(value)) 428 | } 429 | }) 430 | return form 431 | } 432 | 433 | function parseHeaders(rawHeaders) { 434 | var headers = new Headers() 435 | // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space 436 | // https://tools.ietf.org/html/rfc7230#section-3.2 437 | var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') 438 | // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill 439 | // https://github.com/github/fetch/issues/748 440 | // https://github.com/zloirock/core-js/issues/751 441 | preProcessedHeaders 442 | .split('\r') 443 | .map(function(header) { 444 | return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header 445 | }) 446 | .forEach(function(line) { 447 | var parts = line.split(':') 448 | var key = parts.shift().trim() 449 | if (key) { 450 | var value = parts.join(':').trim() 451 | try { 452 | headers.append(key, value) 453 | } catch (error) { 454 | console.warn('Response ' + error.message) 455 | } 456 | } 457 | }) 458 | return headers 459 | } 460 | 461 | Body.call(Request.prototype) 462 | 463 | export function Response(bodyInit, options) { 464 | if (!(this instanceof Response)) { 465 | throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') 466 | } 467 | if (!options) { 468 | options = {} 469 | } 470 | 471 | this.type = 'default' 472 | this.status = options.status === undefined ? 200 : options.status 473 | if (this.status < 200 || this.status > 599) { 474 | throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") 475 | } 476 | this.ok = this.status >= 200 && this.status < 300 477 | this.statusText = options.statusText === undefined ? '' : '' + options.statusText 478 | this.headers = new Headers(options.headers) 479 | this.url = options.url || '' 480 | this._initBody(bodyInit) 481 | } 482 | 483 | Body.call(Response.prototype) 484 | 485 | Response.prototype.clone = function() { 486 | return new Response(this._bodyInit, { 487 | status: this.status, 488 | statusText: this.statusText, 489 | headers: new Headers(this.headers), 490 | url: this.url 491 | }) 492 | } 493 | 494 | Response.error = function() { 495 | var response = new Response(null, {status: 200, statusText: ''}) 496 | response.ok = false 497 | response.status = 0 498 | response.type = 'error' 499 | return response 500 | } 501 | 502 | var redirectStatuses = [301, 302, 303, 307, 308] 503 | 504 | Response.redirect = function(url, status) { 505 | if (redirectStatuses.indexOf(status) === -1) { 506 | throw new RangeError('Invalid status code') 507 | } 508 | 509 | return new Response(null, {status: status, headers: {location: url}}) 510 | } 511 | 512 | export var DOMException = g.DOMException 513 | try { 514 | new DOMException() 515 | } catch (err) { 516 | DOMException = function(message, name) { 517 | this.message = message 518 | this.name = name 519 | var error = Error(message) 520 | this.stack = error.stack 521 | } 522 | DOMException.prototype = Object.create(Error.prototype) 523 | DOMException.prototype.constructor = DOMException 524 | } 525 | 526 | export function fetch(input, init) { 527 | return new Promise(function(resolve, reject) { 528 | var request = new Request(input, init) 529 | 530 | if (request.signal && request.signal.aborted) { 531 | return reject(new DOMException('Aborted', 'AbortError')) 532 | } 533 | 534 | var xhr = new XMLHttpRequest() 535 | 536 | function abortXhr() { 537 | xhr.abort() 538 | } 539 | 540 | xhr.onload = function() { 541 | var options = { 542 | statusText: xhr.statusText, 543 | headers: parseHeaders(xhr.getAllResponseHeaders() || '') 544 | } 545 | // This check if specifically for when a user fetches a file locally from the file system 546 | // Only if the status is out of a normal range 547 | if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { 548 | options.status = 200; 549 | } else { 550 | options.status = xhr.status; 551 | } 552 | options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') 553 | var body = 'response' in xhr ? xhr.response : xhr.responseText 554 | setTimeout(function() { 555 | resolve(new Response(body, options)) 556 | }, 0) 557 | } 558 | 559 | xhr.onerror = function() { 560 | setTimeout(function() { 561 | reject(new TypeError('Network request failed')) 562 | }, 0) 563 | } 564 | 565 | xhr.ontimeout = function() { 566 | setTimeout(function() { 567 | reject(new TypeError('Network request timed out')) 568 | }, 0) 569 | } 570 | 571 | xhr.onabort = function() { 572 | setTimeout(function() { 573 | reject(new DOMException('Aborted', 'AbortError')) 574 | }, 0) 575 | } 576 | 577 | function fixUrl(url) { 578 | try { 579 | return url === '' && g.location.href ? g.location.href : url 580 | } catch (e) { 581 | return url 582 | } 583 | } 584 | 585 | xhr.open(request.method, fixUrl(request.url), true) 586 | 587 | if (request.credentials === 'include') { 588 | xhr.withCredentials = true 589 | } else if (request.credentials === 'omit') { 590 | xhr.withCredentials = false 591 | } 592 | 593 | if ('responseType' in xhr) { 594 | if (support.blob) { 595 | xhr.responseType = 'blob' 596 | } else if ( 597 | support.arrayBuffer 598 | ) { 599 | xhr.responseType = 'arraybuffer' 600 | } 601 | } 602 | 603 | if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { 604 | var names = []; 605 | Object.getOwnPropertyNames(init.headers).forEach(function(name) { 606 | names.push(normalizeName(name)) 607 | xhr.setRequestHeader(name, normalizeValue(init.headers[name])) 608 | }) 609 | request.headers.forEach(function(value, name) { 610 | if (names.indexOf(name) === -1) { 611 | xhr.setRequestHeader(name, value) 612 | } 613 | }) 614 | } else { 615 | request.headers.forEach(function(value, name) { 616 | xhr.setRequestHeader(name, value) 617 | }) 618 | } 619 | 620 | if (request.signal) { 621 | request.signal.addEventListener('abort', abortXhr) 622 | 623 | xhr.onreadystatechange = function() { 624 | // DONE (success or failure) 625 | if (xhr.readyState === 4) { 626 | request.signal.removeEventListener('abort', abortXhr) 627 | } 628 | } 629 | } 630 | 631 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) 632 | }) 633 | } 634 | 635 | fetch.polyfill = true 636 | 637 | if (!g.fetch) { 638 | g.fetch = fetch 639 | g.Headers = Headers 640 | g.Request = Request 641 | g.Response = Response 642 | } 643 | -------------------------------------------------------------------------------- /fetch.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | type CredentialsType = 'omit' | 'same-origin' | 'include' 4 | 5 | type ResponseType = 'default' | 'error' 6 | 7 | type BodyInit = string | URLSearchParams | FormData | Blob | ArrayBuffer | $ArrayBufferView 8 | 9 | type RequestInfo = Request | URL | string 10 | 11 | type RequestOptions = {| 12 | body?: ?BodyInit; 13 | 14 | credentials?: CredentialsType; 15 | headers?: HeadersInit; 16 | method?: string; 17 | mode?: string; 18 | referrer?: string; 19 | signal?: ?AbortSignal; 20 | |} 21 | 22 | type ResponseOptions = {| 23 | status?: number; 24 | statusText?: string; 25 | headers?: HeadersInit; 26 | |} 27 | 28 | type HeadersInit = Headers | {[string]: string} 29 | 30 | // https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L902-L914 31 | declare class Headers { 32 | @@iterator(): Iterator<[string, string]>; 33 | constructor(init?: HeadersInit): void; 34 | append(name: string, value: string): void; 35 | delete(name: string): void; 36 | entries(): Iterator<[string, string]>; 37 | forEach((value: string, name: string, headers: Headers) => any, thisArg?: any): void; 38 | get(name: string): null | string; 39 | has(name: string): boolean; 40 | keys(): Iterator; 41 | set(name: string, value: string): void; 42 | values(): Iterator; 43 | } 44 | 45 | // https://github.com/facebook/flow/pull/6548 46 | interface AbortSignal { 47 | aborted: boolean; 48 | addEventListener(type: string, listener: (Event) => mixed, options?: EventListenerOptionsOrUseCapture): void; 49 | removeEventListener(type: string, listener: (Event) => mixed, options?: EventListenerOptionsOrUseCapture): void; 50 | } 51 | 52 | // https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L994-L1018 53 | // unsupported in polyfill: 54 | // - cache 55 | // - integrity 56 | // - redirect 57 | // - referrerPolicy 58 | declare class Request { 59 | constructor(input: RequestInfo, init?: RequestOptions): void; 60 | clone(): Request; 61 | 62 | url: string; 63 | 64 | credentials: CredentialsType; 65 | headers: Headers; 66 | method: string; 67 | mode: ModeType; 68 | referrer: string; 69 | signal: ?AbortSignal; 70 | 71 | // Body methods and attributes 72 | bodyUsed: boolean; 73 | 74 | arrayBuffer(): Promise; 75 | blob(): Promise; 76 | formData(): Promise; 77 | json(): Promise; 78 | text(): Promise; 79 | } 80 | 81 | // https://github.com/facebook/flow/blob/f68b89a5012bd995ab3509e7a41b7325045c4045/lib/bom.js#L968-L992 82 | // unsupported in polyfill: 83 | // - body 84 | // - redirected 85 | // - trailer 86 | declare class Response { 87 | constructor(input?: ?BodyInit, init?: ResponseOptions): void; 88 | clone(): Response; 89 | static error(): Response; 90 | static redirect(url: string, status?: number): Response; 91 | 92 | type: ResponseType; 93 | url: string; 94 | ok: boolean; 95 | status: number; 96 | statusText: string; 97 | headers: Headers; 98 | 99 | // Body methods and attributes 100 | bodyUsed: boolean; 101 | 102 | arrayBuffer(): Promise; 103 | blob(): Promise; 104 | formData(): Promise; 105 | json(): Promise; 106 | text(): Promise; 107 | } 108 | 109 | declare class DOMException extends Error { 110 | constructor(message?: string, name?: string): void; 111 | } 112 | 113 | declare module.exports: { 114 | fetch(input: RequestInfo, init?: RequestOptions): Promise; 115 | Headers: typeof Headers; 116 | Request: typeof Request; 117 | Response: typeof Response; 118 | DOMException: typeof DOMException; 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatwg-fetch", 3 | "description": "A window.fetch polyfill.", 4 | "version": "3.6.20", 5 | "main": "./dist/fetch.umd.js", 6 | "module": "./fetch.js", 7 | "repository": "github/fetch", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "abortcontroller-polyfill": "^1.1.9", 11 | "auto-changelog": "^2.4.0", 12 | "chai": "^4.1.2", 13 | "eslint": "^7.20.0", 14 | "karma": "^3.0.0", 15 | "karma-chai": "^0.1.0", 16 | "karma-chrome-launcher": "^2.2.0", 17 | "karma-detect-browsers": "^2.3.2", 18 | "karma-firefox-launcher": "^1.1.0", 19 | "karma-mocha": "^1.3.0", 20 | "karma-safari-launcher": "^1.0.0", 21 | "karma-safaritechpreview-launcher": "0.0.6", 22 | "mocha": "^4.0.1", 23 | "prettier": "^1.19.1", 24 | "promise-polyfill": "6.0.2", 25 | "rollup": "^0.59.1", 26 | "url-search-params": "0.6.1" 27 | }, 28 | "files": [ 29 | "LICENSE", 30 | "dist/fetch.umd.js", 31 | "dist/fetch.umd.js.flow", 32 | "fetch.js", 33 | "fetch.js.flow" 34 | ], 35 | "scripts": { 36 | "karma": "karma start ./test/karma.config.js --no-single-run --auto-watch", 37 | "prepare": "make dist/fetch.umd.js dist/fetch.umd.js.flow", 38 | "pretest": "make", 39 | "test": "karma start ./test/karma.config.js && karma start ./test/karma-worker.config.js", 40 | "version": "auto-changelog -p && git add CHANGELOG.md" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = require('eslint-plugin-github/prettier.config') 3 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'fetch.js', 3 | output: { 4 | file: 'dist/fetch.umd.js', 5 | format: 'umd', 6 | name: 'WHATWGFetch' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/karma-worker.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const parentConfig = require('./karma.config') 3 | 4 | module.exports = function(config) { 5 | parentConfig(config) 6 | config.set({ 7 | frameworks: ['detectBrowsers', 'mocha'], 8 | files: [ 9 | 'test/worker-adapter.js', 10 | { 11 | pattern: '{test,dist}/*.js', 12 | included: false 13 | }, 14 | { 15 | pattern: 'node_modules/{mocha,chai,abortcontroller-polyfill/dist}/*.js', 16 | included: false, 17 | watched: false 18 | } 19 | ] 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const serverEndpoints = require('./server') 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '..', 7 | frameworks: ['detectBrowsers', 'mocha', 'chai'], 8 | detectBrowsers: { 9 | preferHeadless: true, 10 | usePhantomJS: false, 11 | postDetection: availableBrowsers => 12 | availableBrowsers 13 | .filter( 14 | browser => 15 | !process.env.CI || !browser.startsWith('Chromium') || !availableBrowsers.some(b => b.startsWith('Chrome')) 16 | ) 17 | .map(browser => (browser.startsWith('Chrom') ? `${browser}NoSandbox` : browser)) 18 | }, 19 | client: { 20 | mocha: { 21 | ui: 'tdd' 22 | } 23 | }, 24 | files: [ 25 | 'node_modules/promise-polyfill/promise.js', 26 | 'node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js', 27 | 'node_modules/url-search-params/build/url-search-params.max.js', 28 | 'dist/fetch.umd.js', 29 | 'test/test.js' 30 | ], 31 | reporters: process.env.CI ? ['dots'] : ['progress'], 32 | port: 9876, 33 | colors: true, 34 | logLevel: process.env.CI ? config.LOG_WARN : config.LOG_INFO, 35 | autoWatch: false, 36 | singleRun: true, 37 | concurrency: Infinity, 38 | customLaunchers: { 39 | ChromeHeadlessNoSandbox: { 40 | base: 'ChromeHeadless', 41 | flags: ['--no-sandbox'] 42 | }, 43 | ChromiumHeadlessNoSandbox: { 44 | base: 'ChromiumHeadless', 45 | flags: ['--no-sandbox'] 46 | } 47 | }, 48 | beforeMiddleware: ['custom'], 49 | plugins: [ 50 | 'karma-*', 51 | { 52 | 'middleware:custom': ['value', serverEndpoints] 53 | } 54 | ] 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const url = require('url') 3 | const querystring = require('querystring') 4 | 5 | const routes = { 6 | '/request': function(res, req) { 7 | res.writeHead(200, {'Content-Type': 'application/json'}) 8 | var data = '' 9 | req.on('data', function(c) { 10 | data += c 11 | }) 12 | req.on('end', function() { 13 | res.end( 14 | JSON.stringify({ 15 | method: req.method, 16 | url: req.url, 17 | headers: req.headers, 18 | data: data 19 | }) 20 | ) 21 | }) 22 | }, 23 | '/hello': function(res, req) { 24 | res.writeHead(200, { 25 | 'Content-Type': 'text/plain', 26 | 'X-Request-URL': 'http://' + req.headers.host + req.url 27 | }) 28 | res.end('hi') 29 | }, 30 | '/hello/utf8': function(res) { 31 | res.writeHead(200, { 32 | 'Content-Type': 'text/plain; charset=utf-8' 33 | }) 34 | // "hello" 35 | var buf = Buffer.from([104, 101, 108, 108, 111]) 36 | res.end(buf) 37 | }, 38 | '/hello/utf16le': function(res) { 39 | res.writeHead(200, { 40 | 'Content-Type': 'text/plain; charset=utf-16le' 41 | }) 42 | // "hello" 43 | var buf = Buffer.from([104, 0, 101, 0, 108, 0, 108, 0, 111, 0]) 44 | res.end(buf) 45 | }, 46 | '/binary': function(res) { 47 | res.writeHead(200, {'Content-Type': 'application/octet-stream'}) 48 | var buf = Buffer.alloc(256) 49 | for (var i = 0; i < 256; i++) { 50 | buf[i] = i 51 | } 52 | res.end(buf) 53 | }, 54 | '/redirect/301': function(res) { 55 | res.writeHead(301, {Location: '/hello'}) 56 | res.end() 57 | }, 58 | '/redirect/302': function(res) { 59 | res.writeHead(302, {Location: '/hello'}) 60 | res.end() 61 | }, 62 | '/redirect/303': function(res) { 63 | res.writeHead(303, {Location: '/hello'}) 64 | res.end() 65 | }, 66 | '/redirect/307': function(res) { 67 | res.writeHead(307, {Location: '/hello'}) 68 | res.end() 69 | }, 70 | '/redirect/308': function(res) { 71 | res.writeHead(308, {Location: '/hello'}) 72 | res.end() 73 | }, 74 | '/boom': function(res) { 75 | res.writeHead(500, {'Content-Type': 'text/plain'}) 76 | res.end('boom') 77 | }, 78 | '/empty': function(res) { 79 | res.writeHead(204) 80 | res.end() 81 | }, 82 | '/slow': function(res) { 83 | setTimeout(function() { 84 | res.writeHead(200, {'Cache-Control': 'no-cache, must-revalidate'}) 85 | res.end() 86 | }, 100) 87 | }, 88 | '/error': function(res) { 89 | res.destroy() 90 | }, 91 | '/form': function(res) { 92 | res.writeHead(200, {'Content-Type': 'application/x-www-form-urlencoded'}) 93 | res.end('number=1&space=one+two&empty=&encoded=a%2Bb&') 94 | }, 95 | '/json': function(res) { 96 | res.writeHead(200, {'Content-Type': 'application/json'}) 97 | res.end(JSON.stringify({name: 'Hubot', login: 'hubot'})) 98 | }, 99 | '/json-error': function(res) { 100 | res.writeHead(200, {'Content-Type': 'application/json'}) 101 | res.end('not json {') 102 | }, 103 | '/cookie': function(res, req) { 104 | var setCookie, cookie 105 | var params = querystring.parse(url.parse(req.url).query) 106 | if (params.name && params.value) { 107 | setCookie = [params.name, params.value].join('=') 108 | } 109 | if (params.name) { 110 | cookie = querystring.parse(req.headers['cookie'], '; ')[params.name] 111 | } 112 | res.writeHead(200, { 113 | 'Content-Type': 'text/plain', 114 | 'Set-Cookie': setCookie || '' 115 | }) 116 | res.end(cookie) 117 | }, 118 | '/headers': function(res) { 119 | res.writeHead(200, { 120 | Date: 'Mon, 13 Oct 2014 21:02:27 GMT', 121 | 'Content-Type': 'text/html; charset=utf-8' 122 | }) 123 | res.end() 124 | } 125 | } 126 | 127 | module.exports = function(req, res, next) { 128 | const path = url.parse(req.url).pathname 129 | const route = routes[path] 130 | if (route) { 131 | route(res, req) 132 | } else { 133 | next() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* globals chai assert FileReaderSync assert WHATWGFetch */ 3 | var IEorEdge = /Edge\//.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent) 4 | var Chrome = /Chrome\//.test(navigator.userAgent) && !IEorEdge 5 | var Safari = /Safari\//.test(navigator.userAgent) && !IEorEdge && !Chrome 6 | 7 | var support = { 8 | url: (function(url) { 9 | try { 10 | return new URL(url).toString() === url 11 | } catch (e) { 12 | return false 13 | } 14 | })('http://example.com/'), 15 | blob: 16 | 'FileReader' in self && 17 | 'Blob' in self && 18 | (function() { 19 | try { 20 | new Blob() 21 | return true 22 | } catch (e) { 23 | return false 24 | } 25 | })(), 26 | formData: 'FormData' in self, 27 | arrayBuffer: 'ArrayBuffer' in self, 28 | aborting: 'signal' in new Request(''), 29 | permanentRedirect: !/Trident/.test(navigator.userAgent) 30 | } 31 | 32 | function readBlobAsText(blob) { 33 | if ('FileReader' in self) { 34 | return new Promise(function(resolve, reject) { 35 | var reader = new FileReader() 36 | reader.onload = function() { 37 | resolve(reader.result) 38 | } 39 | reader.onerror = function() { 40 | reject(reader.error) 41 | } 42 | reader.readAsText(blob) 43 | }) 44 | } else if ('FileReaderSync' in self) { 45 | return new FileReaderSync().readAsText(blob) 46 | } else { 47 | throw new ReferenceError('FileReader is not defined') 48 | } 49 | } 50 | 51 | function readBlobAsBytes(blob) { 52 | if ('FileReader' in self) { 53 | return new Promise(function(resolve, reject) { 54 | var reader = new FileReader() 55 | reader.onload = function() { 56 | var view = new Uint8Array(reader.result) 57 | resolve(Array.prototype.slice.call(view)) 58 | } 59 | reader.onerror = function() { 60 | reject(reader.error) 61 | } 62 | reader.readAsArrayBuffer(blob) 63 | }) 64 | } else if ('FileReaderSync' in self) { 65 | return new FileReaderSync().readAsArrayBuffer(blob) 66 | } else { 67 | throw new ReferenceError('FileReader is not defined') 68 | } 69 | } 70 | 71 | function arrayBufferFromText(text) { 72 | var buf = new ArrayBuffer(text.length) 73 | var view = new Uint8Array(buf) 74 | 75 | for (var i = 0; i < text.length; i++) { 76 | view[i] = text.charCodeAt(i) 77 | } 78 | return buf 79 | } 80 | 81 | function readArrayBufferAsText(buf) { 82 | var view = new Uint8Array(buf) 83 | var chars = new Array(view.length) 84 | 85 | for (var i = 0; i < view.length; i++) { 86 | chars[i] = String.fromCharCode(view[i]) 87 | } 88 | return chars.join('') 89 | } 90 | 91 | var preservedGlobals = {} 92 | var keepGlobals = ['fetch', 'Headers', 'Request', 'Response'] 93 | var exercise = ['polyfill'] 94 | 95 | // If native fetch implementation exists, replace it with the polyfilled 96 | // version at first. The native implementation will be restored before the 97 | // additional `native` pass of the test suite. 98 | if (!self.fetch.polyfill) { 99 | keepGlobals.forEach(function(name) { 100 | preservedGlobals[name] = self[name] 101 | self[name] = WHATWGFetch[name] 102 | }) 103 | exercise.push('native') 104 | } 105 | 106 | var slice = Array.prototype.slice 107 | 108 | function featureDependent(testOrSuite, condition) { 109 | (condition ? testOrSuite : testOrSuite.skip).apply(this, slice.call(arguments, 2)) 110 | } 111 | 112 | exercise.forEach(function(exerciseMode) { 113 | suite(exerciseMode, function() { 114 | if (exerciseMode === 'native') { 115 | suiteSetup(function() { 116 | keepGlobals.forEach(function(name) { 117 | self[name] = preservedGlobals[name] 118 | }) 119 | }) 120 | } 121 | 122 | var nativeChrome = Chrome && exerciseMode === 'native' 123 | var nativeSafari = Safari && exerciseMode === 'native' 124 | var nativeEdge = /Edge\//.test(navigator.userAgent) && exerciseMode === 'native' 125 | var firefox = navigator.userAgent.match(/Firefox\/(\d+)/) 126 | var brokenFF = firefox && firefox[1] <= 56 && exerciseMode === 'native' 127 | var emptyDefaultStatusText = 128 | exerciseMode !== 'native' || (exerciseMode === 'native' && (Chrome || (firefox && firefox[1] >= 67))) 129 | var polyfillFirefox = firefox && exerciseMode === 'polyfill' 130 | var omitSafari = 131 | Safari && exerciseMode === 'native' && navigator.userAgent.match(/Version\/(\d+\.\d+)/)[1] <= '11.1' 132 | 133 | // https://fetch.spec.whatwg.org/#concept-bodyinit-extract 134 | function testBodyExtract(factory) { 135 | suite('body extract', function() { 136 | var expected = 'Hello World!' 137 | var inputs = [['type USVString', expected]] 138 | if (support.blob) { 139 | inputs.push(['type Blob', new Blob([expected])]) 140 | } 141 | if (support.arrayBuffer) { 142 | inputs = inputs.concat([ 143 | ['type ArrayBuffer', arrayBufferFromText(expected)], 144 | ['type TypedArray', new Uint8Array(arrayBufferFromText(expected))], 145 | ['type DataView', new DataView(arrayBufferFromText(expected))] 146 | ]) 147 | } 148 | 149 | inputs.forEach(function(input) { 150 | var typeLabel = input[0], 151 | body = input[1] 152 | 153 | suite(typeLabel, function() { 154 | featureDependent(test, support.blob, 'consume as blob', function() { 155 | var r = factory(body) 156 | return r 157 | .blob() 158 | .then(readBlobAsText) 159 | .then(function(text) { 160 | assert.equal(text, expected) 161 | }) 162 | }) 163 | 164 | test('consume as text', function() { 165 | var r = factory(body) 166 | return r.text().then(function(text) { 167 | assert.equal(text, expected) 168 | }) 169 | }) 170 | 171 | featureDependent(test, support.arrayBuffer, 'consume as array buffer', function() { 172 | var r = factory(body) 173 | return r 174 | .arrayBuffer() 175 | .then(readArrayBufferAsText) 176 | .then(function(text) { 177 | assert.equal(text, expected) 178 | }) 179 | }) 180 | }) 181 | }) 182 | }) 183 | } 184 | 185 | // https://fetch.spec.whatwg.org/#headers-class 186 | suite('Headers', function() { 187 | test('constructor copies headers', function() { 188 | var original = new Headers() 189 | original.append('Accept', 'application/json') 190 | original.append('Accept', 'text/plain') 191 | original.append('Content-Type', 'text/html') 192 | 193 | var headers = new Headers(original) 194 | assert.equal(headers.get('Accept'), 'application/json, text/plain') 195 | assert.equal(headers.get('Content-type'), 'text/html') 196 | }) 197 | test('constructor works with arrays', function() { 198 | var array = [ 199 | ['Content-Type', 'text/xml'], 200 | ['Breaking-Bad', '<3'] 201 | ] 202 | var headers = new Headers(array) 203 | 204 | assert.equal(headers.get('Content-Type'), 'text/xml') 205 | assert.equal(headers.get('Breaking-Bad'), '<3') 206 | assert.throws(function() { 207 | new Headers([ 208 | ['Content-Type'], 209 | ]) 210 | }, TypeError) 211 | assert.throws(function() { 212 | new Headers([ 213 | ['Content-Type', 'a', 'b'], 214 | ]) 215 | }, TypeError) 216 | }) 217 | test('headers are case insensitive', function() { 218 | var headers = new Headers({Accept: 'application/json'}) 219 | assert.equal(headers.get('ACCEPT'), 'application/json') 220 | assert.equal(headers.get('Accept'), 'application/json') 221 | assert.equal(headers.get('accept'), 'application/json') 222 | }) 223 | test('appends to existing', function() { 224 | var headers = new Headers({Accept: 'application/json'}) 225 | assert.isFalse(headers.has('Content-Type')) 226 | headers.append('Content-Type', 'application/json') 227 | assert.isTrue(headers.has('Content-Type')) 228 | assert.equal(headers.get('Content-Type'), 'application/json') 229 | }) 230 | test('appends values to existing header name', function() { 231 | var headers = new Headers({Accept: 'application/json'}) 232 | headers.append('Accept', 'text/plain') 233 | assert.equal(headers.get('Accept'), 'application/json, text/plain') 234 | }) 235 | test('sets header name and value', function() { 236 | var headers = new Headers() 237 | headers.set('Content-Type', 'application/json') 238 | assert.equal(headers.get('Content-Type'), 'application/json') 239 | }) 240 | test('returns null on no header found', function() { 241 | var headers = new Headers() 242 | assert.isNull(headers.get('Content-Type')) 243 | }) 244 | test('has headers that are set', function() { 245 | var headers = new Headers() 246 | headers.set('Content-Type', 'application/json') 247 | assert.isTrue(headers.has('Content-Type')) 248 | }) 249 | test('deletes headers', function() { 250 | var headers = new Headers() 251 | headers.set('Content-Type', 'application/json') 252 | assert.isTrue(headers.has('Content-Type')) 253 | headers.delete('Content-Type') 254 | assert.isFalse(headers.has('Content-Type')) 255 | assert.isNull(headers.get('Content-Type')) 256 | }) 257 | test('converts field name to string on set and get', function() { 258 | var headers = new Headers() 259 | headers.set(1, 'application/json') 260 | assert.isTrue(headers.has('1')) 261 | assert.equal(headers.get(1), 'application/json') 262 | }) 263 | test('converts field value to string on set and get', function() { 264 | var headers = new Headers() 265 | headers.set('Content-Type', 1) 266 | headers.set('X-CSRF-Token', undefined) 267 | assert.equal(headers.get('Content-Type'), '1') 268 | assert.equal(headers.get('X-CSRF-Token'), 'undefined') 269 | }) 270 | test('throws TypeError on invalid character in field name', function() { 271 | assert.throws(function() { 272 | new Headers({'[Accept]': 'application/json'}) 273 | }, TypeError) 274 | assert.throws(function() { 275 | new Headers({'Accept:': 'application/json'}) 276 | }, TypeError) 277 | assert.throws(function() { 278 | var headers = new Headers() 279 | headers.set({field: 'value'}, 'application/json') 280 | }, TypeError) 281 | assert.throws(function() { 282 | new Headers({'': 'application/json'}) 283 | }, TypeError) 284 | }) 285 | featureDependent(test, !brokenFF, 'is iterable with forEach', function() { 286 | var headers = new Headers() 287 | headers.append('Accept', 'application/json') 288 | headers.append('Accept', 'text/plain') 289 | headers.append('Content-Type', 'text/html') 290 | 291 | var results = [] 292 | headers.forEach(function(value, key, object) { 293 | results.push({value: value, key: key, object: object}) 294 | }) 295 | 296 | assert.equal(results.length, 2) 297 | assert.deepEqual({key: 'accept', value: 'application/json, text/plain', object: headers}, results[0]) 298 | assert.deepEqual({key: 'content-type', value: 'text/html', object: headers}, results[1]) 299 | }) 300 | test('forEach accepts second thisArg argument', function() { 301 | var headers = new Headers({Accept: 'application/json'}) 302 | var thisArg = 42 303 | headers.forEach(function() { 304 | assert.equal(this, thisArg) 305 | }, thisArg) 306 | }) 307 | featureDependent(test, !brokenFF, 'is iterable with keys', function() { 308 | var headers = new Headers() 309 | headers.append('Accept', 'application/json') 310 | headers.append('Accept', 'text/plain') 311 | headers.append('Content-Type', 'text/html') 312 | 313 | var iterator = headers.keys() 314 | assert.deepEqual({done: false, value: 'accept'}, iterator.next()) 315 | assert.deepEqual({done: false, value: 'content-type'}, iterator.next()) 316 | assert.deepEqual({done: true, value: undefined}, iterator.next()) 317 | }) 318 | featureDependent(test, !brokenFF, 'is iterable with values', function() { 319 | var headers = new Headers() 320 | headers.append('Accept', 'application/json') 321 | headers.append('Accept', 'text/plain') 322 | headers.append('Content-Type', 'text/html') 323 | 324 | var iterator = headers.values() 325 | assert.deepEqual({done: false, value: 'application/json, text/plain'}, iterator.next()) 326 | assert.deepEqual({done: false, value: 'text/html'}, iterator.next()) 327 | assert.deepEqual({done: true, value: undefined}, iterator.next()) 328 | }) 329 | featureDependent(test, !brokenFF, 'is iterable with entries', function() { 330 | var headers = new Headers() 331 | headers.append('Accept', 'application/json') 332 | headers.append('Accept', 'text/plain') 333 | headers.append('Content-Type', 'text/html') 334 | 335 | var iterator = headers.entries() 336 | assert.deepEqual({done: false, value: ['accept', 'application/json, text/plain']}, iterator.next()) 337 | assert.deepEqual({done: false, value: ['content-type', 'text/html']}, iterator.next()) 338 | assert.deepEqual({done: true, value: undefined}, iterator.next()) 339 | }) 340 | }) 341 | 342 | // https://fetch.spec.whatwg.org/#request-class 343 | suite('Request', function() { 344 | test('called as normal function', function() { 345 | assert.throws(function() { 346 | Request('https://fetch.spec.whatwg.org/') 347 | }) 348 | }) 349 | test('construct with string url', function() { 350 | var request = new Request('https://fetch.spec.whatwg.org/') 351 | assert.equal(request.url, 'https://fetch.spec.whatwg.org/') 352 | }) 353 | 354 | featureDependent(test, support.url, 'construct with URL instance', function() { 355 | var url = new URL('https://fetch.spec.whatwg.org/') 356 | url.pathname = 'cors' 357 | var request = new Request(url) 358 | assert.equal(request.url, 'https://fetch.spec.whatwg.org/cors') 359 | }) 360 | 361 | test('construct with non-Request object', function() { 362 | var url = { 363 | toString: function() { 364 | return 'https://fetch.spec.whatwg.org/' 365 | } 366 | } 367 | var request = new Request(url) 368 | assert.equal(request.url, 'https://fetch.spec.whatwg.org/') 369 | }) 370 | 371 | test('construct with Request', function() { 372 | var request1 = new Request('https://fetch.spec.whatwg.org/', { 373 | method: 'post', 374 | body: 'I work out', 375 | headers: { 376 | accept: 'application/json', 377 | 'Content-Type': 'text/plain' 378 | } 379 | }) 380 | var request2 = new Request(request1) 381 | 382 | return request2.text().then(function(body2) { 383 | assert.equal(body2, 'I work out') 384 | assert.equal(request2.method, 'POST') 385 | assert.equal(request2.url, 'https://fetch.spec.whatwg.org/') 386 | assert.equal(request2.headers.get('accept'), 'application/json') 387 | assert.equal(request2.headers.get('content-type'), 'text/plain') 388 | 389 | return request1.text().then( 390 | function() { 391 | assert(false, 'original request body should have been consumed') 392 | }, 393 | function(error) { 394 | assert(error instanceof TypeError, 'expected TypeError for already read body') 395 | } 396 | ) 397 | }) 398 | }) 399 | 400 | test('construct with Request and override headers', function() { 401 | var request1 = new Request('https://fetch.spec.whatwg.org/', { 402 | method: 'post', 403 | body: 'I work out', 404 | headers: { 405 | accept: 'application/json', 406 | 'X-Request-ID': '123' 407 | } 408 | }) 409 | var request2 = new Request(request1, { 410 | headers: {'x-test': '42'} 411 | }) 412 | 413 | assert.equal(request2.headers.get('accept'), undefined) 414 | assert.equal(request2.headers.get('x-request-id'), undefined) 415 | assert.equal(request2.headers.get('x-test'), '42') 416 | }) 417 | 418 | test('construct with Request and override body', function() { 419 | var request1 = new Request('https://fetch.spec.whatwg.org/', { 420 | method: 'post', 421 | body: 'I work out', 422 | headers: { 423 | 'Content-Type': 'text/plain' 424 | } 425 | }) 426 | var request2 = new Request(request1, { 427 | body: '{"wiggles": 5}', 428 | headers: {'Content-Type': 'application/json'} 429 | }) 430 | 431 | return request2.json().then(function(data) { 432 | assert.equal(data.wiggles, 5) 433 | assert.equal(request2.headers.get('content-type'), 'application/json') 434 | }) 435 | }) 436 | 437 | featureDependent(test, !nativeChrome, 'construct with used Request body', function() { 438 | var request1 = new Request('https://fetch.spec.whatwg.org/', { 439 | method: 'post', 440 | body: 'I work out' 441 | }) 442 | 443 | return request1.text().then(function() { 444 | assert.throws(function() { 445 | new Request(request1) 446 | }, TypeError) 447 | }) 448 | }) 449 | 450 | test('GET should not have implicit Content-Type', function() { 451 | var req = new Request('https://fetch.spec.whatwg.org/') 452 | assert.equal(req.headers.get('content-type'), undefined) 453 | }) 454 | 455 | test('POST with blank body should not have implicit Content-Type', function() { 456 | var req = new Request('https://fetch.spec.whatwg.org/', { 457 | method: 'post' 458 | }) 459 | assert.equal(req.headers.get('content-type'), undefined) 460 | }) 461 | 462 | test('construct with string body sets Content-Type header', function() { 463 | var req = new Request('https://fetch.spec.whatwg.org/', { 464 | method: 'post', 465 | body: 'I work out' 466 | }) 467 | 468 | assert.equal(req.headers.get('content-type'), 'text/plain;charset=UTF-8') 469 | }) 470 | 471 | featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() { 472 | var req = new Request('https://fetch.spec.whatwg.org/', { 473 | method: 'post', 474 | body: new Blob(['test'], {type: 'image/png'}) 475 | }) 476 | 477 | assert.equal(req.headers.get('content-type'), 'image/png') 478 | }) 479 | 480 | test('construct with body and explicit header uses header', function() { 481 | var req = new Request('https://fetch.spec.whatwg.org/', { 482 | method: 'post', 483 | headers: {'Content-Type': 'image/png'}, 484 | body: 'I work out' 485 | }) 486 | 487 | assert.equal(req.headers.get('content-type'), 'image/png') 488 | }) 489 | 490 | featureDependent(test, support.blob, 'construct with Blob body and explicit Content-Type header', function() { 491 | var req = new Request('https://fetch.spec.whatwg.org/', { 492 | method: 'post', 493 | headers: {'Content-Type': 'image/png'}, 494 | body: new Blob(['test'], {type: 'text/plain'}) 495 | }) 496 | 497 | assert.equal(req.headers.get('content-type'), 'image/png') 498 | }) 499 | 500 | featureDependent(test, !IEorEdge, 'construct with URLSearchParams body sets Content-Type header', function() { 501 | var req = new Request('https://fetch.spec.whatwg.org/', { 502 | method: 'post', 503 | body: new URLSearchParams('a=1&b=2') 504 | }) 505 | 506 | assert.equal(req.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8') 507 | }) 508 | 509 | featureDependent( 510 | test, 511 | !IEorEdge, 512 | 'construct with URLSearchParams body and explicit Content-Type header', 513 | function() { 514 | var req = new Request('https://fetch.spec.whatwg.org/', { 515 | method: 'post', 516 | headers: {'Content-Type': 'image/png'}, 517 | body: new URLSearchParams('a=1&b=2') 518 | }) 519 | 520 | assert.equal(req.headers.get('content-type'), 'image/png') 521 | } 522 | ) 523 | 524 | test('construct with unsupported body type', function() { 525 | var req = new Request('https://fetch.spec.whatwg.org/', { 526 | method: 'post', 527 | body: {} 528 | }) 529 | 530 | assert.equal(req.headers.get('content-type'), 'text/plain;charset=UTF-8') 531 | return req.text().then(function(bodyText) { 532 | assert.equal(bodyText, '[object Object]') 533 | }) 534 | }) 535 | 536 | test('construct with null body', function() { 537 | var req = new Request('https://fetch.spec.whatwg.org/', { 538 | method: 'post' 539 | }) 540 | 541 | assert.isNull(req.headers.get('content-type')) 542 | return req.text().then(function(bodyText) { 543 | assert.equal(bodyText, '') 544 | }) 545 | }) 546 | 547 | test('clone GET request', function() { 548 | var req = new Request('https://fetch.spec.whatwg.org/', { 549 | headers: {'content-type': 'text/plain'} 550 | }) 551 | var clone = req.clone() 552 | 553 | assert.equal(clone.url, req.url) 554 | assert.equal(clone.method, 'GET') 555 | assert.equal(clone.headers.get('content-type'), 'text/plain') 556 | assert.notEqual(clone.headers, req.headers) 557 | assert.isFalse(req.bodyUsed) 558 | }) 559 | 560 | test('clone POST request', function() { 561 | var req = new Request('https://fetch.spec.whatwg.org/', { 562 | method: 'post', 563 | headers: {'content-type': 'text/plain'}, 564 | body: 'I work out' 565 | }) 566 | var clone = req.clone() 567 | 568 | assert.equal(clone.method, 'POST') 569 | assert.equal(clone.headers.get('content-type'), 'text/plain') 570 | assert.notEqual(clone.headers, req.headers) 571 | assert.equal(req.bodyUsed, false) 572 | 573 | return Promise.all([clone.text(), req.clone().text()]).then(function(bodies) { 574 | assert.deepEqual(bodies, ['I work out', 'I work out']) 575 | }) 576 | }) 577 | 578 | featureDependent(test, !nativeChrome, 'clone with used Request body', function() { 579 | var req = new Request('https://fetch.spec.whatwg.org/', { 580 | method: 'post', 581 | body: 'I work out' 582 | }) 583 | 584 | return req.text().then(function() { 585 | assert.throws(function() { 586 | req.clone() 587 | }, TypeError) 588 | }) 589 | }) 590 | 591 | testBodyExtract(function(body) { 592 | return new Request('', {method: 'POST', body: body}) 593 | }) 594 | 595 | featureDependent(test, !omitSafari, 'credentials defaults to same-origin', function() { 596 | var request = new Request('') 597 | assert.equal(request.credentials, 'same-origin') 598 | }) 599 | 600 | test('credentials is overridable', function() { 601 | var request = new Request('', {credentials: 'omit'}) 602 | assert.equal(request.credentials, 'omit') 603 | }) 604 | }) 605 | 606 | // https://fetch.spec.whatwg.org/#response-class 607 | suite('Response', function() { 608 | featureDependent(test, emptyDefaultStatusText, 'default status is 200', function() { 609 | var res = new Response() 610 | assert.equal(res.status, 200) 611 | assert.equal(res.statusText, '') 612 | assert.isTrue(res.ok) 613 | }) 614 | 615 | featureDependent( 616 | test, 617 | emptyDefaultStatusText, 618 | 'default status is 200 when an explicit undefined status code is passed', 619 | function() { 620 | var res = new Response('', {status: undefined}) 621 | assert.equal(res.status, 200) 622 | assert.equal(res.statusText, '') 623 | assert.isTrue(res.ok) 624 | } 625 | ) 626 | 627 | testBodyExtract(function(body) { 628 | return new Response(body) 629 | }) 630 | 631 | test('called as normal function', function() { 632 | assert.throws(function() { 633 | Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}}) 634 | }) 635 | }) 636 | test('status outside inclusive range 200-599 ', function() { 637 | assert.throws(function() { 638 | new Response('', {status: 199}) 639 | }) 640 | for (var i = 0; i < 200; i++) { 641 | assert.throws(function() { 642 | new Response('', {status: i}) 643 | }) 644 | } 645 | for (i = 999; i > 599; i--) { 646 | assert.throws(function() { 647 | new Response('', {status: i}) 648 | }) 649 | } 650 | // A fetch with the url of a `file://` scheme may have a status 0 or 651 | // similar in some operating systems. In the event that a status is found outside 652 | // the standard range of 200-599, and the url start with `file://` 653 | // the status should return 200 654 | assert.doesNotThrow(function() { 655 | new Request('', {status: 0, request: {url: 'file://path/to/local/file'}}) 656 | }) 657 | }) 658 | 659 | test('creates Headers object from raw headers', function() { 660 | var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}}) 661 | assert.equal(r.headers instanceof Headers, true) 662 | return r.json().then(function(json) { 663 | assert.equal(json.foo, 'bar') 664 | return json 665 | }) 666 | }) 667 | 668 | test('always creates a new Headers instance', function() { 669 | var headers = new Headers({'x-hello': 'world'}) 670 | var res = new Response('', {headers: headers}) 671 | 672 | assert.equal(res.headers.get('x-hello'), 'world') 673 | assert.notEqual(res.headers, headers) 674 | }) 675 | 676 | test('clone text response', function() { 677 | var res = new Response('{"foo":"bar"}', { 678 | headers: {'content-type': 'application/json'} 679 | }) 680 | var clone = res.clone() 681 | 682 | assert.notEqual(clone.headers, res.headers, 'headers were cloned') 683 | assert.equal(clone.headers.get('content-type'), 'application/json') 684 | 685 | return Promise.all([clone.json(), res.json()]).then(function(jsons) { 686 | assert.deepEqual(jsons[0], jsons[1], 'json of cloned object is the same as original') 687 | }) 688 | }) 689 | 690 | featureDependent(test, support.blob, 'clone blob response', function() { 691 | var req = new Request(new Blob(['test'])) 692 | req.clone() 693 | assert.equal(req.bodyUsed, false) 694 | }) 695 | 696 | test('error creates error Response', function() { 697 | var r = Response.error() 698 | assert(r instanceof Response) 699 | assert.equal(r.ok, false) 700 | assert.equal(r.status, 0) 701 | assert.equal(r.statusText, '') 702 | assert.equal(r.type, 'error') 703 | }) 704 | 705 | test('redirect creates redirect Response', function() { 706 | var r = Response.redirect('https://fetch.spec.whatwg.org/', 301) 707 | assert(r instanceof Response) 708 | assert.equal(r.status, 301) 709 | assert.equal(r.headers.get('Location'), 'https://fetch.spec.whatwg.org/') 710 | }) 711 | 712 | test('construct with string body sets Content-Type header', function() { 713 | var r = new Response('I work out') 714 | assert.equal(r.headers.get('content-type'), 'text/plain;charset=UTF-8') 715 | }) 716 | 717 | featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() { 718 | var r = new Response(new Blob(['test'], {type: 'text/plain'})) 719 | assert.equal(r.headers.get('content-type'), 'text/plain') 720 | }) 721 | 722 | test('construct with body and explicit header uses header', function() { 723 | var r = new Response('I work out', { 724 | headers: { 725 | 'Content-Type': 'text/plain' 726 | } 727 | }) 728 | 729 | assert.equal(r.headers.get('content-type'), 'text/plain') 730 | }) 731 | 732 | test('construct with undefined statusText', function() { 733 | var r = new Response('', {statusText: undefined}) 734 | assert.equal(r.statusText, '') 735 | }) 736 | 737 | test('construct with null statusText', function() { 738 | var r = new Response('', {statusText: null}) 739 | assert.equal(r.statusText, 'null') 740 | }) 741 | 742 | test('init object as first argument', function() { 743 | var r = new Response({ 744 | status: 201, 745 | headers: { 746 | 'Content-Type': 'text/html' 747 | } 748 | }) 749 | 750 | assert.equal(r.status, 200) 751 | assert.equal(r.headers.get('content-type'), 'text/plain;charset=UTF-8') 752 | return r.text().then(function(bodyText) { 753 | assert.equal(bodyText, '[object Object]') 754 | }) 755 | }) 756 | 757 | test('null as first argument', function() { 758 | var r = new Response(null) 759 | 760 | assert.isNull(r.headers.get('content-type')) 761 | return r.text().then(function(bodyText) { 762 | assert.equal(bodyText, '') 763 | }) 764 | }) 765 | }) 766 | 767 | // https://fetch.spec.whatwg.org/#body-mixin 768 | suite('Body mixin', function() { 769 | featureDependent(suite, support.blob, 'arrayBuffer', function() { 770 | test('resolves arrayBuffer promise', function() { 771 | return fetch('/hello') 772 | .then(function(response) { 773 | return response.arrayBuffer() 774 | }) 775 | .then(function(buf) { 776 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 777 | assert.equal(buf.byteLength, 2) 778 | }) 779 | }) 780 | 781 | test('arrayBuffer handles binary data', function() { 782 | return fetch('/binary') 783 | .then(function(response) { 784 | return response.arrayBuffer() 785 | }) 786 | .then(function(buf) { 787 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 788 | assert.equal(buf.byteLength, 256, 'buf.byteLength is correct') 789 | var view = new Uint8Array(buf) 790 | for (var i = 0; i < 256; i++) { 791 | assert.equal(view[i], i) 792 | } 793 | }) 794 | }) 795 | 796 | test('arrayBuffer handles utf-8 data', function() { 797 | return fetch('/hello/utf8') 798 | .then(function(response) { 799 | return response.arrayBuffer() 800 | }) 801 | .then(function(buf) { 802 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 803 | assert.equal(buf.byteLength, 5, 'buf.byteLength is correct') 804 | var octets = Array.prototype.slice.call(new Uint8Array(buf)) 805 | assert.deepEqual(octets, [104, 101, 108, 108, 111]) 806 | }) 807 | }) 808 | 809 | test('arrayBuffer handles utf-16le data', function() { 810 | return fetch('/hello/utf16le') 811 | .then(function(response) { 812 | return response.arrayBuffer() 813 | }) 814 | .then(function(buf) { 815 | assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance') 816 | assert.equal(buf.byteLength, 10, 'buf.byteLength is correct') 817 | var octets = Array.prototype.slice.call(new Uint8Array(buf)) 818 | assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0]) 819 | }) 820 | }) 821 | 822 | test('rejects arrayBuffer promise after body is consumed', function() { 823 | return fetch('/hello') 824 | .then(function(response) { 825 | assert.equal(response.bodyUsed, false) 826 | response.blob() 827 | assert.equal(response.bodyUsed, true) 828 | return response.arrayBuffer() 829 | }) 830 | .catch(function(error) { 831 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 832 | }) 833 | }) 834 | }) 835 | 836 | featureDependent(suite, support.blob, 'blob', function() { 837 | test('resolves blob promise', function() { 838 | return fetch('/hello') 839 | .then(function(response) { 840 | return response.blob() 841 | }) 842 | .then(function(blob) { 843 | assert(blob instanceof Blob, 'blob is a Blob instance') 844 | assert.equal(blob.size, 2) 845 | }) 846 | }) 847 | 848 | test('blob handles binary data', function() { 849 | return fetch('/binary') 850 | .then(function(response) { 851 | return response.blob() 852 | }) 853 | .then(function(blob) { 854 | assert(blob instanceof Blob, 'blob is a Blob instance') 855 | assert.equal(blob.size, 256, 'blob.size is correct') 856 | }) 857 | }) 858 | 859 | test('blob handles utf-8 data', function() { 860 | return fetch('/hello/utf8') 861 | .then(function(response) { 862 | return response.blob() 863 | }) 864 | .then(readBlobAsBytes) 865 | .then(function(octets) { 866 | assert.equal(octets.length, 5, 'blob.size is correct') 867 | assert.deepEqual(octets, [104, 101, 108, 108, 111]) 868 | }) 869 | }) 870 | 871 | test('blob handles utf-16le data', function() { 872 | return fetch('/hello/utf16le') 873 | .then(function(response) { 874 | return response.blob() 875 | }) 876 | .then(readBlobAsBytes) 877 | .then(function(octets) { 878 | assert.equal(octets.length, 10, 'blob.size is correct') 879 | assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0]) 880 | }) 881 | }) 882 | 883 | test('rejects blob promise after body is consumed', function() { 884 | return fetch('/hello') 885 | .then(function(response) { 886 | assert(response.blob, 'Body does not implement blob') 887 | assert.equal(response.bodyUsed, false) 888 | response.text() 889 | assert.equal(response.bodyUsed, true) 890 | return response.blob() 891 | }) 892 | .catch(function(error) { 893 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 894 | }) 895 | }) 896 | }) 897 | 898 | featureDependent(suite, support.formData, 'formData', function() { 899 | test('post sets content-type header', function() { 900 | return fetch('/request', { 901 | method: 'post', 902 | body: new FormData() 903 | }) 904 | .then(function(response) { 905 | return response.json() 906 | }) 907 | .then(function(json) { 908 | assert.equal(json.method, 'POST') 909 | assert(/^multipart\/form-data;/.test(json.headers['content-type'])) 910 | }) 911 | }) 912 | 913 | featureDependent(test, !nativeChrome && !nativeEdge, 'formData rejects after body was consumed', function() { 914 | return fetch('/json') 915 | .then(function(response) { 916 | assert(response.formData, 'Body does not implement formData') 917 | response.formData() 918 | return response.formData() 919 | }) 920 | .catch(function(error) { 921 | if (error instanceof chai.AssertionError) { 922 | throw error 923 | } else { 924 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 925 | } 926 | }) 927 | }) 928 | 929 | featureDependent( 930 | test, 931 | !nativeChrome && !nativeSafari && !nativeEdge, 932 | 'parses form encoded response', 933 | function() { 934 | return fetch('/form') 935 | .then(function(response) { 936 | return response.formData() 937 | }) 938 | .then(function(form) { 939 | assert(form instanceof FormData, 'Parsed a FormData object') 940 | }) 941 | } 942 | ) 943 | }) 944 | 945 | suite('json', function() { 946 | test('parses json response', function() { 947 | return fetch('/json') 948 | .then(function(response) { 949 | return response.json() 950 | }) 951 | .then(function(json) { 952 | assert.equal(json.name, 'Hubot') 953 | assert.equal(json.login, 'hubot') 954 | }) 955 | }) 956 | 957 | test('rejects json promise after body is consumed', function() { 958 | return fetch('/json') 959 | .then(function(response) { 960 | assert(response.json, 'Body does not implement json') 961 | assert.equal(response.bodyUsed, false) 962 | response.text() 963 | assert.equal(response.bodyUsed, true) 964 | return response.json() 965 | }) 966 | .catch(function(error) { 967 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 968 | }) 969 | }) 970 | 971 | featureDependent(test, !polyfillFirefox, 'handles json parse error', function() { 972 | return fetch('/json-error') 973 | .then(function(response) { 974 | return response.json() 975 | }) 976 | .catch(function(error) { 977 | if (!IEorEdge) assert(error instanceof Error, 'JSON exception is an Error instance') 978 | assert(error.message, 'JSON exception has an error message') 979 | }) 980 | }) 981 | }) 982 | 983 | suite('text', function() { 984 | test('handles 204 No Content response', function() { 985 | return fetch('/empty') 986 | .then(function(response) { 987 | assert.equal(response.status, 204) 988 | return response.text() 989 | }) 990 | .then(function(body) { 991 | assert.equal(body, '') 992 | }) 993 | }) 994 | 995 | test('resolves text promise', function() { 996 | return fetch('/hello') 997 | .then(function(response) { 998 | return response.text() 999 | }) 1000 | .then(function(text) { 1001 | assert.equal(text, 'hi') 1002 | }) 1003 | }) 1004 | 1005 | test('rejects text promise after body is consumed', function() { 1006 | return fetch('/hello') 1007 | .then(function(response) { 1008 | assert(response.text, 'Body does not implement text') 1009 | assert.equal(response.bodyUsed, false) 1010 | response.text() 1011 | assert.equal(response.bodyUsed, true) 1012 | return response.text() 1013 | }) 1014 | .catch(function(error) { 1015 | assert(error instanceof TypeError, 'Promise rejected after body consumed') 1016 | }) 1017 | }) 1018 | 1019 | test('does not set bodyUsed to true if no body supplied', function() { 1020 | var response = new Response(); 1021 | assert(response.text, 'Body does not implement text') 1022 | assert.equal(response.bodyUsed, false) 1023 | response.text() 1024 | assert.equal(response.bodyUsed, false) 1025 | return response.text() 1026 | }) 1027 | }) 1028 | }) 1029 | 1030 | suite('fetch method', function() { 1031 | suite('promise resolution', function() { 1032 | test('resolves promise on 500 error', function() { 1033 | return fetch('/boom') 1034 | .then(function(response) { 1035 | assert.equal(response.status, 500) 1036 | assert.equal(response.ok, false) 1037 | return response.text() 1038 | }) 1039 | .then(function(body) { 1040 | assert.equal(body, 'boom') 1041 | }) 1042 | }) 1043 | 1044 | test.skip('rejects promise for network error', function() { 1045 | return fetch('/error') 1046 | .then(function(response) { 1047 | assert(false, 'HTTP status ' + response.status + ' was treated as success') 1048 | }) 1049 | .catch(function(error) { 1050 | assert(error instanceof TypeError, 'Rejected with Error') 1051 | }) 1052 | }) 1053 | 1054 | test('rejects when Request constructor throws', function() { 1055 | return fetch('/request', {method: 'GET', body: 'invalid'}) 1056 | .then(function() { 1057 | assert(false, 'Invalid Request init was accepted') 1058 | }) 1059 | .catch(function(error) { 1060 | assert(error instanceof TypeError, 'Rejected with Error') 1061 | }) 1062 | }) 1063 | }) 1064 | 1065 | suite('request', function() { 1066 | test('sends headers', function() { 1067 | return fetch('/request', { 1068 | headers: { 1069 | Accept: 'application/json', 1070 | 'X-Test': '42' 1071 | } 1072 | }) 1073 | .then(function(response) { 1074 | return response.json() 1075 | }) 1076 | .then(function(json) { 1077 | assert.equal(json.headers['accept'], 'application/json') 1078 | assert.equal(json.headers['x-test'], '42') 1079 | }) 1080 | }) 1081 | 1082 | test('with Request as argument', function() { 1083 | var request = new Request('/request', { 1084 | headers: { 1085 | Accept: 'application/json', 1086 | 'X-Test': '42' 1087 | } 1088 | }) 1089 | 1090 | return fetch(request) 1091 | .then(function(response) { 1092 | return response.json() 1093 | }) 1094 | .then(function(json) { 1095 | assert.equal(json.headers['accept'], 'application/json') 1096 | assert.equal(json.headers['x-test'], '42') 1097 | }) 1098 | }) 1099 | 1100 | test('reusing same Request multiple times', function() { 1101 | var request = new Request('/request', { 1102 | headers: { 1103 | Accept: 'application/json', 1104 | 'X-Test': '42' 1105 | } 1106 | }) 1107 | 1108 | var responses = [] 1109 | 1110 | return fetch(request) 1111 | .then(function(response) { 1112 | responses.push(response) 1113 | return fetch(request) 1114 | }) 1115 | .then(function(response) { 1116 | responses.push(response) 1117 | return fetch(request) 1118 | }) 1119 | .then(function(response) { 1120 | responses.push(response) 1121 | return Promise.all( 1122 | responses.map(function(r) { 1123 | return r.json() 1124 | }) 1125 | ) 1126 | }) 1127 | .then(function(jsons) { 1128 | jsons.forEach(function(json) { 1129 | assert.equal(json.headers['accept'], 'application/json') 1130 | assert.equal(json.headers['x-test'], '42') 1131 | }) 1132 | }) 1133 | }) 1134 | 1135 | featureDependent(suite, support.arrayBuffer, 'ArrayBuffer', function() { 1136 | test('ArrayBuffer body', function() { 1137 | return fetch('/request', { 1138 | method: 'post', 1139 | body: arrayBufferFromText('name=Hubot') 1140 | }) 1141 | .then(function(response) { 1142 | return response.json() 1143 | }) 1144 | .then(function(request) { 1145 | assert.equal(request.method, 'POST') 1146 | assert.equal(request.data, 'name=Hubot') 1147 | }) 1148 | }) 1149 | 1150 | test('DataView body', function() { 1151 | return fetch('/request', { 1152 | method: 'post', 1153 | body: new DataView(arrayBufferFromText('name=Hubot')) 1154 | }) 1155 | .then(function(response) { 1156 | return response.json() 1157 | }) 1158 | .then(function(request) { 1159 | assert.equal(request.method, 'POST') 1160 | assert.equal(request.data, 'name=Hubot') 1161 | }) 1162 | }) 1163 | 1164 | test('TypedArray body', function() { 1165 | return fetch('/request', { 1166 | method: 'post', 1167 | body: new Uint8Array(arrayBufferFromText('name=Hubot')) 1168 | }) 1169 | .then(function(response) { 1170 | return response.json() 1171 | }) 1172 | .then(function(request) { 1173 | assert.equal(request.method, 'POST') 1174 | assert.equal(request.data, 'name=Hubot') 1175 | }) 1176 | }) 1177 | }) 1178 | 1179 | featureDependent(test, !IEorEdge, 'sends URLSearchParams body', function() { 1180 | return fetch('/request', { 1181 | method: 'post', 1182 | body: new URLSearchParams('a=1&b=2') 1183 | }) 1184 | .then(function(response) { 1185 | return response.json() 1186 | }) 1187 | .then(function(request) { 1188 | assert.equal(request.method, 'POST') 1189 | assert.equal(request.data, 'a=1&b=2') 1190 | }) 1191 | }) 1192 | }) 1193 | 1194 | featureDependent(suite, exerciseMode !== 'native' || support.aborting, 'aborting', function() { 1195 | test('Request init creates an AbortSignal without option', function() { 1196 | var request = new Request('/request') 1197 | assert.ok(request.signal); 1198 | assert.equal(request.signal.aborted, false); 1199 | }) 1200 | 1201 | test('Request init passes AbortSignal from option', function () { 1202 | var controller = new AbortController() 1203 | var request = new Request('/request', {signal: controller.signal}) 1204 | assert.ok(request.signal); 1205 | assert.deepEqual(controller.signal, request.signal); 1206 | }) 1207 | 1208 | test('initially aborted signal', function () { 1209 | var controller = new AbortController() 1210 | controller.abort() 1211 | 1212 | return fetch('/request', { 1213 | signal: controller.signal 1214 | }).then( 1215 | function() { 1216 | assert.ok(false) 1217 | }, 1218 | function(error) { 1219 | if (!IEorEdge) assert.instanceOf(error, WHATWGFetch.DOMException) 1220 | assert.equal(error.name, 'AbortError') 1221 | } 1222 | ) 1223 | }) 1224 | 1225 | test('initially aborted signal within Request', function() { 1226 | var controller = new AbortController() 1227 | controller.abort() 1228 | 1229 | var request = new Request('/request', {signal: controller.signal}) 1230 | 1231 | return fetch(request).then( 1232 | function() { 1233 | assert.ok(false) 1234 | }, 1235 | function(error) { 1236 | assert.equal(error.name, 'AbortError') 1237 | } 1238 | ) 1239 | }) 1240 | 1241 | test('mid-request', function() { 1242 | var controller = new AbortController() 1243 | 1244 | setTimeout(function() { 1245 | controller.abort() 1246 | }, 30) 1247 | 1248 | return fetch('/slow?_=' + new Date().getTime(), { 1249 | signal: controller.signal 1250 | }).then( 1251 | function() { 1252 | assert.ok(false) 1253 | }, 1254 | function(error) { 1255 | assert.equal(error.name, 'AbortError') 1256 | } 1257 | ) 1258 | }) 1259 | 1260 | test('mid-request within Request', function() { 1261 | var controller = new AbortController() 1262 | var request = new Request('/slow?_=' + new Date().getTime(), {signal: controller.signal}) 1263 | 1264 | setTimeout(function() { 1265 | controller.abort() 1266 | }, 30) 1267 | 1268 | return fetch(request).then( 1269 | function() { 1270 | assert.ok(false) 1271 | }, 1272 | function(error) { 1273 | assert.equal(error.name, 'AbortError') 1274 | } 1275 | ) 1276 | }) 1277 | 1278 | test('abort multiple with same signal', function() { 1279 | var controller = new AbortController() 1280 | 1281 | setTimeout(function() { 1282 | controller.abort() 1283 | }, 30) 1284 | 1285 | return Promise.all([ 1286 | fetch('/slow?_=' + new Date().getTime(), { 1287 | signal: controller.signal 1288 | }).then( 1289 | function() { 1290 | assert.ok(false) 1291 | }, 1292 | function(error) { 1293 | assert.equal(error.name, 'AbortError') 1294 | } 1295 | ), 1296 | fetch('/slow?_=' + new Date().getTime(), { 1297 | signal: controller.signal 1298 | }).then( 1299 | function() { 1300 | assert.ok(false) 1301 | }, 1302 | function(error) { 1303 | assert.equal(error.name, 'AbortError') 1304 | } 1305 | ) 1306 | ]) 1307 | }) 1308 | }) 1309 | 1310 | suite('response', function() { 1311 | test('populates body', function() { 1312 | return fetch('/hello') 1313 | .then(function(response) { 1314 | assert.equal(response.status, 200) 1315 | assert.equal(response.ok, true) 1316 | return response.text() 1317 | }) 1318 | .then(function(body) { 1319 | assert.equal(body, 'hi') 1320 | }) 1321 | }) 1322 | 1323 | test('parses headers', function() { 1324 | return fetch('/headers?' + new Date().getTime()).then(function(response) { 1325 | assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT') 1326 | assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8') 1327 | }) 1328 | }) 1329 | }) 1330 | 1331 | // https://fetch.spec.whatwg.org/#methods 1332 | suite('HTTP methods', function() { 1333 | test('supports HTTP GET', function() { 1334 | return fetch('/request', { 1335 | method: 'get' 1336 | }) 1337 | .then(function(response) { 1338 | return response.json() 1339 | }) 1340 | .then(function(request) { 1341 | assert.equal(request.method, 'GET') 1342 | assert.equal(request.data, '') 1343 | }) 1344 | }) 1345 | 1346 | test('GET with body throws TypeError', function() { 1347 | assert.throw(function() { 1348 | new Request('', { 1349 | method: 'get', 1350 | body: 'invalid' 1351 | }) 1352 | }, TypeError) 1353 | }) 1354 | 1355 | test('HEAD with body throws TypeError', function() { 1356 | assert.throw(function() { 1357 | new Request('', { 1358 | method: 'head', 1359 | body: 'invalid' 1360 | }) 1361 | }, TypeError) 1362 | }) 1363 | 1364 | test('supports HTTP POST', function() { 1365 | return fetch('/request', { 1366 | method: 'post', 1367 | body: 'name=Hubot' 1368 | }) 1369 | .then(function(response) { 1370 | return response.json() 1371 | }) 1372 | .then(function(request) { 1373 | assert.equal(request.method, 'POST') 1374 | assert.equal(request.data, 'name=Hubot') 1375 | }) 1376 | }) 1377 | 1378 | test('supports HTTP PUT', function() { 1379 | return fetch('/request', { 1380 | method: 'put', 1381 | body: 'name=Hubot' 1382 | }) 1383 | .then(function(response) { 1384 | return response.json() 1385 | }) 1386 | .then(function(request) { 1387 | assert.equal(request.method, 'PUT') 1388 | assert.equal(request.data, 'name=Hubot') 1389 | }) 1390 | }) 1391 | 1392 | test('supports HTTP PATCH', function() { 1393 | return fetch('/request', { 1394 | method: 'PATCH', 1395 | body: 'name=Hubot' 1396 | }) 1397 | .then(function(response) { 1398 | return response.json() 1399 | }) 1400 | .then(function(request) { 1401 | assert.equal(request.method, 'PATCH') 1402 | assert.equal(request.data, 'name=Hubot') 1403 | }) 1404 | }) 1405 | 1406 | test('supports HTTP DELETE', function() { 1407 | return fetch('/request', { 1408 | method: 'delete' 1409 | }) 1410 | .then(function(response) { 1411 | return response.json() 1412 | }) 1413 | .then(function(request) { 1414 | assert.equal(request.method, 'DELETE') 1415 | assert.equal(request.data, '') 1416 | }) 1417 | }) 1418 | }) 1419 | 1420 | // https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 1421 | suite('Atomic HTTP redirect handling', function() { 1422 | test('handles 301 redirect response', function() { 1423 | return fetch('/redirect/301') 1424 | .then(function(response) { 1425 | assert.equal(response.status, 200) 1426 | assert.equal(response.ok, true) 1427 | assert.match(response.url, /\/hello/) 1428 | return response.text() 1429 | }) 1430 | .then(function(body) { 1431 | assert.equal(body, 'hi') 1432 | }) 1433 | }) 1434 | 1435 | test('handles 302 redirect response', function() { 1436 | return fetch('/redirect/302') 1437 | .then(function(response) { 1438 | assert.equal(response.status, 200) 1439 | assert.equal(response.ok, true) 1440 | assert.match(response.url, /\/hello/) 1441 | return response.text() 1442 | }) 1443 | .then(function(body) { 1444 | assert.equal(body, 'hi') 1445 | }) 1446 | }) 1447 | 1448 | test('handles 303 redirect response', function() { 1449 | return fetch('/redirect/303') 1450 | .then(function(response) { 1451 | assert.equal(response.status, 200) 1452 | assert.equal(response.ok, true) 1453 | assert.match(response.url, /\/hello/) 1454 | return response.text() 1455 | }) 1456 | .then(function(body) { 1457 | assert.equal(body, 'hi') 1458 | }) 1459 | }) 1460 | 1461 | test('handles 307 redirect response', function() { 1462 | return fetch('/redirect/307') 1463 | .then(function(response) { 1464 | assert.equal(response.status, 200) 1465 | assert.equal(response.ok, true) 1466 | assert.match(response.url, /\/hello/) 1467 | return response.text() 1468 | }) 1469 | .then(function(body) { 1470 | assert.equal(body, 'hi') 1471 | }) 1472 | }) 1473 | 1474 | featureDependent(test, support.permanentRedirect, 'handles 308 redirect response', function() { 1475 | return fetch('/redirect/308') 1476 | .then(function(response) { 1477 | assert.equal(response.status, 200) 1478 | assert.equal(response.ok, true) 1479 | assert.match(response.url, /\/hello/) 1480 | return response.text() 1481 | }) 1482 | .then(function(body) { 1483 | assert.equal(body, 'hi') 1484 | }) 1485 | }) 1486 | }) 1487 | 1488 | // https://fetch.spec.whatwg.org/#concept-request-credentials-mode 1489 | suite('credentials mode', function() { 1490 | setup(function() { 1491 | return fetch('/cookie?name=foo&value=reset', {credentials: 'same-origin'}) 1492 | }) 1493 | 1494 | featureDependent(suite, exerciseMode === 'native', 'omit', function() { 1495 | test('does not accept cookies with omit credentials', function() { 1496 | return fetch('/cookie?name=foo&value=bar', {credentials: 'omit'}) 1497 | .then(function() { 1498 | return fetch('/cookie?name=foo', {credentials: 'same-origin'}) 1499 | }) 1500 | .then(function(response) { 1501 | return response.text() 1502 | }) 1503 | .then(function(data) { 1504 | assert.equal(data, 'reset') 1505 | }) 1506 | }) 1507 | 1508 | test('does not send cookies with omit credentials', function() { 1509 | return fetch('/cookie?name=foo&value=bar') 1510 | .then(function() { 1511 | return fetch('/cookie?name=foo', {credentials: 'omit'}) 1512 | }) 1513 | .then(function(response) { 1514 | return response.text() 1515 | }) 1516 | .then(function(data) { 1517 | assert.equal(data, '') 1518 | }) 1519 | }) 1520 | }) 1521 | 1522 | suite('same-origin', function() { 1523 | test('send cookies with same-origin credentials', function() { 1524 | return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'}) 1525 | .then(function() { 1526 | return fetch('/cookie?name=foo', {credentials: 'same-origin'}) 1527 | }) 1528 | .then(function(response) { 1529 | return response.text() 1530 | }) 1531 | .then(function(data) { 1532 | assert.equal(data, 'bar') 1533 | }) 1534 | }) 1535 | }) 1536 | 1537 | suite('include', function() { 1538 | test('send cookies with include credentials', function() { 1539 | return fetch('/cookie?name=foo&value=bar', {credentials: 'include'}) 1540 | .then(function() { 1541 | return fetch('/cookie?name=foo', {credentials: 'include'}) 1542 | }) 1543 | .then(function(response) { 1544 | return response.text() 1545 | }) 1546 | .then(function(data) { 1547 | assert.equal(data, 'bar') 1548 | }) 1549 | }) 1550 | }) 1551 | }) 1552 | }) 1553 | }) 1554 | }) 1555 | -------------------------------------------------------------------------------- /test/worker-adapter.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* globals Mocha */ 3 | var mochaRun = mocha.run 4 | mocha.run = function() {} 5 | 6 | mocha.suite.suites.unshift(Mocha.Suite.create(mocha.suite, 'worker')) 7 | 8 | var worker = new Worker('/base/test/worker.js') 9 | 10 | worker.addEventListener('message', function(e) { 11 | switch (e.data.name) { 12 | case 'pass': 13 | test(e.data.title, function() {}) 14 | break 15 | case 'pending': 16 | test(e.data.title) 17 | break 18 | case 'fail': 19 | test(e.data.title, function() { 20 | var err = new Error(e.data.message) 21 | err.stack = e.data.stack 22 | throw err 23 | }) 24 | break 25 | case 'end': 26 | mochaRun() 27 | break 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-env worker */ 2 | /* globals mocha chai */ 3 | importScripts('/base/node_modules/mocha/mocha.js') 4 | importScripts('/base/node_modules/chai/chai.js') 5 | 6 | mocha.setup('tdd') 7 | self.assert = chai.assert 8 | 9 | importScripts('/base/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js') 10 | importScripts('/base/dist/fetch.umd.js') 11 | importScripts('/base/test/test.js') 12 | 13 | function title(test) { 14 | return test.fullTitle().replace(/#/g, '') 15 | } 16 | 17 | function reporter(runner) { 18 | runner.on('pending', function(test) { 19 | self.postMessage({name: 'pending', title: title(test)}) 20 | }) 21 | 22 | runner.on('pass', function(test) { 23 | self.postMessage({name: 'pass', title: title(test)}) 24 | }) 25 | 26 | runner.on('fail', function(test, err) { 27 | self.postMessage({ 28 | name: 'fail', 29 | title: title(test), 30 | message: err.message, 31 | stack: err.stack 32 | }) 33 | }) 34 | 35 | runner.on('end', function() { 36 | self.postMessage({name: 'end'}) 37 | }) 38 | } 39 | 40 | mocha.reporter(reporter).run() 41 | --------------------------------------------------------------------------------